blob: 381d8dbdcf819b4d2773fc8ce0ef1447e682da7d [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070087 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000121 try:
tandrii5d48c322016-08-18 16:19:37 -0700122 (out, _), code = subprocess2.communicate(['git'] + args,
123 env=GetNoGitPagerEnv(),
124 stdout=subprocess2.PIPE,
125 stderr=stderr)
126 return code, out
127 except subprocess2.CalledProcessError as e:
128 logging.debug('Failed running %s', args)
129 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000133 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
tandrii5d48c322016-08-18 16:19:37 -0700159def _git_branch_config_key(branch, key):
160 """Helper method to return Git config key for a branch."""
161 assert branch, 'branch name is required to set git config for it'
162 return 'branch.%s.%s' % (branch, key)
163
164
165def _git_get_branch_config_value(key, default=None, value_type=str,
166 branch=False):
167 """Returns git config value of given or current branch if any.
168
169 Returns default in all other cases.
170 """
171 assert value_type in (int, str, bool)
172 if branch is False: # Distinguishing default arg value from None.
173 branch = GetCurrentBranch()
174
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000175 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700176 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000177
tandrii5d48c322016-08-18 16:19:37 -0700178 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700179 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700180 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700181 # git config also has --int, but apparently git config suffers from integer
182 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700183 args.append(_git_branch_config_key(branch, key))
184 code, out = RunGitWithCode(args)
185 if code == 0:
186 value = out.strip()
187 if value_type == int:
188 return int(value)
189 if value_type == bool:
190 return bool(value.lower() == 'true')
191 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000192 return default
193
194
tandrii5d48c322016-08-18 16:19:37 -0700195def _git_set_branch_config_value(key, value, branch=None, **kwargs):
196 """Sets the value or unsets if it's None of a git branch config.
197
198 Valid, though not necessarily existing, branch must be provided,
199 otherwise currently checked out branch is used.
200 """
201 if not branch:
202 branch = GetCurrentBranch()
203 assert branch, 'a branch name OR currently checked out branch is required'
204 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700205 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700206 if value is None:
207 args.append('--unset')
208 elif isinstance(value, bool):
209 args.append('--bool')
210 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700211 else:
tandrii33a46ff2016-08-23 05:53:40 -0700212 # git config also has --int, but apparently git config suffers from integer
213 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700214 value = str(value)
215 args.append(_git_branch_config_key(branch, key))
216 if value is not None:
217 args.append(value)
218 RunGit(args, **kwargs)
219
220
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000221def add_git_similarity(parser):
222 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700223 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000224 help='Sets the percentage that a pair of files need to match in order to'
225 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000226 parser.add_option(
227 '--find-copies', action='store_true',
228 help='Allows git to look for copies.')
229 parser.add_option(
230 '--no-find-copies', action='store_false', dest='find_copies',
231 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000232
233 old_parser_args = parser.parse_args
234 def Parse(args):
235 options, args = old_parser_args(args)
236
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000237 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700238 options.similarity = _git_get_branch_config_value(
239 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000240 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241 print('Note: Saving similarity of %d%% in git config.'
242 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700243 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000244
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245 options.similarity = max(0, min(options.similarity, 100))
246
247 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.find_copies = _git_get_branch_config_value(
249 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 else:
tandrii5d48c322016-08-18 16:19:37 -0700251 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000252
253 print('Using %d%% similarity for rename/copy detection. '
254 'Override with --similarity.' % options.similarity)
255
256 return options, args
257 parser.parse_args = Parse
258
259
machenbach@chromium.org45453142015-09-15 08:45:22 +0000260def _get_properties_from_options(options):
261 properties = dict(x.split('=', 1) for x in options.properties)
262 for key, val in properties.iteritems():
263 try:
264 properties[key] = json.loads(val)
265 except ValueError:
266 pass # If a value couldn't be evaluated, treat it as a string.
267 return properties
268
269
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000270def _prefix_master(master):
271 """Convert user-specified master name to full master name.
272
273 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
274 name, while the developers always use shortened master name
275 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
276 function does the conversion for buildbucket migration.
277 """
278 prefix = 'master.'
279 if master.startswith(prefix):
280 return master
281 return '%s%s' % (prefix, master)
282
283
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000284def _buildbucket_retry(operation_name, http, *args, **kwargs):
285 """Retries requests to buildbucket service and returns parsed json content."""
286 try_count = 0
287 while True:
288 response, content = http.request(*args, **kwargs)
289 try:
290 content_json = json.loads(content)
291 except ValueError:
292 content_json = None
293
294 # Buildbucket could return an error even if status==200.
295 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000296 error = content_json.get('error')
297 if error.get('code') == 403:
298 raise BuildbucketResponseException(
299 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000300 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000301 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000302 raise BuildbucketResponseException(msg)
303
304 if response.status == 200:
305 if not content_json:
306 raise BuildbucketResponseException(
307 'Buildbucket returns invalid json content: %s.\n'
308 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
309 content)
310 return content_json
311 if response.status < 500 or try_count >= 2:
312 raise httplib2.HttpLib2Error(content)
313
314 # status >= 500 means transient failures.
315 logging.debug('Transient errors when %s. Will retry.', operation_name)
316 time.sleep(0.5 + 1.5*try_count)
317 try_count += 1
318 assert False, 'unreachable'
319
320
machenbach@chromium.org45453142015-09-15 08:45:22 +0000321def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000322 rietveld_url = settings.GetDefaultServerUrl()
323 rietveld_host = urlparse.urlparse(rietveld_url).hostname
324 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
325 http = authenticator.authorize(httplib2.Http())
326 http.force_exception_to_status_code = True
327 issue_props = changelist.GetIssueProperties()
328 issue = changelist.GetIssue()
329 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000330 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332 buildbucket_put_url = (
333 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000334 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
336 hostname=rietveld_host,
337 issue=issue,
338 patch=patchset)
339
340 batch_req_body = {'builds': []}
341 print_text = []
342 print_text.append('Tried jobs on:')
343 for master, builders_and_tests in sorted(masters.iteritems()):
344 print_text.append('Master: %s' % master)
345 bucket = _prefix_master(master)
346 for builder, tests in sorted(builders_and_tests.iteritems()):
347 print_text.append(' %s: %s' % (builder, tests))
348 parameters = {
349 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000350 'changes': [{
351 'author': {'email': issue_props['owner_email']},
352 'revision': options.revision,
353 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'properties': {
355 'category': category,
356 'issue': issue,
357 'master': master,
358 'patch_project': issue_props['project'],
359 'patch_storage': 'rietveld',
360 'patchset': patchset,
361 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000362 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 },
364 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000365 if 'presubmit' in builder.lower():
366 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000367 if tests:
368 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000369 if properties:
370 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371 if options.clobber:
372 parameters['properties']['clobber'] = True
373 batch_req_body['builds'].append(
374 {
375 'bucket': bucket,
376 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000377 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000378 'tags': ['builder:%s' % builder,
379 'buildset:%s' % buildset,
380 'master:%s' % master,
381 'user_agent:git_cl_try']
382 }
383 )
384
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700386 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 http,
388 buildbucket_put_url,
389 'PUT',
390 body=json.dumps(batch_req_body),
391 headers={'Content-Type': 'application/json'}
392 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000393 print_text.append('To see results here, run: git cl try-results')
394 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700395 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000396
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000397
tandrii221ab252016-10-06 08:12:04 -0700398def fetch_try_jobs(auth_config, changelist, buildbucket_host,
399 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700400 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401
qyearsley53f48a12016-09-01 10:45:13 -0700402 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 """
tandrii221ab252016-10-06 08:12:04 -0700404 assert buildbucket_host
405 assert changelist.GetIssue(), 'CL must be uploaded first'
406 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
407 patchset = patchset or changelist.GetMostRecentPatchset()
408 assert patchset, 'CL must be uploaded first'
409
410 codereview_url = changelist.GetCodereviewServer()
411 codereview_host = urlparse.urlparse(codereview_url).hostname
412 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000413 if authenticator.has_cached_credentials():
414 http = authenticator.authorize(httplib2.Http())
415 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700416 print('Warning: Some results might be missing because %s' %
417 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700418 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 http = httplib2.Http()
420
421 http.force_exception_to_status_code = True
422
tandrii221ab252016-10-06 08:12:04 -0700423 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
424 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
425 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000426 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700427 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000428 params = {'tag': 'buildset:%s' % buildset}
429
430 builds = {}
431 while True:
432 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700433 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000434 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700435 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000436 for build in content.get('builds', []):
437 builds[build['id']] = build
438 if 'next_cursor' in content:
439 params['start_cursor'] = content['next_cursor']
440 else:
441 break
442 return builds
443
444
qyearsleyeab3c042016-08-24 09:18:28 -0700445def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000446 """Prints nicely result of fetch_try_jobs."""
447 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700448 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000449 return
450
451 # Make a copy, because we'll be modifying builds dictionary.
452 builds = builds.copy()
453 builder_names_cache = {}
454
455 def get_builder(b):
456 try:
457 return builder_names_cache[b['id']]
458 except KeyError:
459 try:
460 parameters = json.loads(b['parameters_json'])
461 name = parameters['builder_name']
462 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700463 print('WARNING: failed to get builder name for build %s: %s' % (
464 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000465 name = None
466 builder_names_cache[b['id']] = name
467 return name
468
469 def get_bucket(b):
470 bucket = b['bucket']
471 if bucket.startswith('master.'):
472 return bucket[len('master.'):]
473 return bucket
474
475 if options.print_master:
476 name_fmt = '%%-%ds %%-%ds' % (
477 max(len(str(get_bucket(b))) for b in builds.itervalues()),
478 max(len(str(get_builder(b))) for b in builds.itervalues()))
479 def get_name(b):
480 return name_fmt % (get_bucket(b), get_builder(b))
481 else:
482 name_fmt = '%%-%ds' % (
483 max(len(str(get_builder(b))) for b in builds.itervalues()))
484 def get_name(b):
485 return name_fmt % get_builder(b)
486
487 def sort_key(b):
488 return b['status'], b.get('result'), get_name(b), b.get('url')
489
490 def pop(title, f, color=None, **kwargs):
491 """Pop matching builds from `builds` dict and print them."""
492
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000493 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000494 colorize = str
495 else:
496 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
497
498 result = []
499 for b in builds.values():
500 if all(b.get(k) == v for k, v in kwargs.iteritems()):
501 builds.pop(b['id'])
502 result.append(b)
503 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700504 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000505 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700506 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000507
508 total = len(builds)
509 pop(status='COMPLETED', result='SUCCESS',
510 title='Successes:', color=Fore.GREEN,
511 f=lambda b: (get_name(b), b.get('url')))
512 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
513 title='Infra Failures:', color=Fore.MAGENTA,
514 f=lambda b: (get_name(b), b.get('url')))
515 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
516 title='Failures:', color=Fore.RED,
517 f=lambda b: (get_name(b), b.get('url')))
518 pop(status='COMPLETED', result='CANCELED',
519 title='Canceled:', color=Fore.MAGENTA,
520 f=lambda b: (get_name(b),))
521 pop(status='COMPLETED', result='FAILURE',
522 failure_reason='INVALID_BUILD_DEFINITION',
523 title='Wrong master/builder name:', color=Fore.MAGENTA,
524 f=lambda b: (get_name(b),))
525 pop(status='COMPLETED', result='FAILURE',
526 title='Other failures:',
527 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
528 pop(status='COMPLETED',
529 title='Other finished:',
530 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
531 pop(status='STARTED',
532 title='Started:', color=Fore.YELLOW,
533 f=lambda b: (get_name(b), b.get('url')))
534 pop(status='SCHEDULED',
535 title='Scheduled:',
536 f=lambda b: (get_name(b), 'id=%s' % b['id']))
537 # The last section is just in case buildbucket API changes OR there is a bug.
538 pop(title='Other:',
539 f=lambda b: (get_name(b), 'id=%s' % b['id']))
540 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700541 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542
543
qyearsley53f48a12016-09-01 10:45:13 -0700544def write_try_results_json(output_file, builds):
545 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
546
547 The input |builds| dict is assumed to be generated by Buildbucket.
548 Buildbucket documentation: http://goo.gl/G0s101
549 """
550
551 def convert_build_dict(build):
552 return {
553 'buildbucket_id': build.get('id'),
554 'status': build.get('status'),
555 'result': build.get('result'),
556 'bucket': build.get('bucket'),
557 'builder_name': json.loads(
558 build.get('parameters_json', '{}')).get('builder_name'),
559 'failure_reason': build.get('failure_reason'),
560 'url': build.get('url'),
561 }
562
563 converted = []
564 for _, build in sorted(builds.items()):
565 converted.append(convert_build_dict(build))
566 write_json(output_file, converted)
567
568
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000569def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
570 """Return the corresponding git ref if |base_url| together with |glob_spec|
571 matches the full |url|.
572
573 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
574 """
575 fetch_suburl, as_ref = glob_spec.split(':')
576 if allow_wildcards:
577 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
578 if glob_match:
579 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
580 # "branches/{472,597,648}/src:refs/remotes/svn/*".
581 branch_re = re.escape(base_url)
582 if glob_match.group(1):
583 branch_re += '/' + re.escape(glob_match.group(1))
584 wildcard = glob_match.group(2)
585 if wildcard == '*':
586 branch_re += '([^/]*)'
587 else:
588 # Escape and replace surrounding braces with parentheses and commas
589 # with pipe symbols.
590 wildcard = re.escape(wildcard)
591 wildcard = re.sub('^\\\\{', '(', wildcard)
592 wildcard = re.sub('\\\\,', '|', wildcard)
593 wildcard = re.sub('\\\\}$', ')', wildcard)
594 branch_re += wildcard
595 if glob_match.group(3):
596 branch_re += re.escape(glob_match.group(3))
597 match = re.match(branch_re, url)
598 if match:
599 return re.sub('\*$', match.group(1), as_ref)
600
601 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
602 if fetch_suburl:
603 full_url = base_url + '/' + fetch_suburl
604 else:
605 full_url = base_url
606 if full_url == url:
607 return as_ref
608 return None
609
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000610
iannucci@chromium.org79540052012-10-19 23:15:26 +0000611def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000612 """Prints statistics about the change to the user."""
613 # --no-ext-diff is broken in some versions of Git, so try to work around
614 # this by overriding the environment (but there is still a problem if the
615 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000616 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000617 if 'GIT_EXTERNAL_DIFF' in env:
618 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000619
620 if find_copies:
621 similarity_options = ['--find-copies-harder', '-l100000',
622 '-C%s' % similarity]
623 else:
624 similarity_options = ['-M%s' % similarity]
625
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000626 try:
627 stdout = sys.stdout.fileno()
628 except AttributeError:
629 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000630 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000631 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000632 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000633 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000634
635
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000636class BuildbucketResponseException(Exception):
637 pass
638
639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000640class Settings(object):
641 def __init__(self):
642 self.default_server = None
643 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000644 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 self.is_git_svn = None
646 self.svn_branch = None
647 self.tree_status_url = None
648 self.viewvc_url = None
649 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000650 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000651 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000652 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000653 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000654 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000655 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000656 self.pending_ref_prefix = None
tandriif46c20f2016-09-14 06:17:05 -0700657 self.git_number_footer = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658
659 def LazyUpdateIfNeeded(self):
660 """Updates the settings from a codereview.settings file, if available."""
661 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000662 # The only value that actually changes the behavior is
663 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000664 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000665 error_ok=True
666 ).strip().lower()
667
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000669 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 LoadCodereviewSettingsFromFile(cr_settings_file)
671 self.updated = True
672
673 def GetDefaultServerUrl(self, error_ok=False):
674 if not self.default_server:
675 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000676 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000677 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 if error_ok:
679 return self.default_server
680 if not self.default_server:
681 error_message = ('Could not find settings file. You must configure '
682 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000683 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000684 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685 return self.default_server
686
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000687 @staticmethod
688 def GetRelativeRoot():
689 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000692 if self.root is None:
693 self.root = os.path.abspath(self.GetRelativeRoot())
694 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000696 def GetGitMirror(self, remote='origin'):
697 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000698 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000699 if not os.path.isdir(local_url):
700 return None
701 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
702 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
703 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
704 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
705 if mirror.exists():
706 return mirror
707 return None
708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709 def GetIsGitSvn(self):
710 """Return true if this repo looks like it's using git-svn."""
711 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000712 if self.GetPendingRefPrefix():
713 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
714 self.is_git_svn = False
715 else:
716 # If you have any "svn-remote.*" config keys, we think you're using svn.
717 self.is_git_svn = RunGitWithCode(
718 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 return self.is_git_svn
720
721 def GetSVNBranch(self):
722 if self.svn_branch is None:
723 if not self.GetIsGitSvn():
724 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
725
726 # Try to figure out which remote branch we're based on.
727 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000728 # 1) iterate through our branch history and find the svn URL.
729 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730
731 # regexp matching the git-svn line that contains the URL.
732 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
733
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000734 # We don't want to go through all of history, so read a line from the
735 # pipe at a time.
736 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000737 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
739 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000740 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000741 for line in proc.stdout:
742 match = git_svn_re.match(line)
743 if match:
744 url = match.group(1)
745 proc.stdout.close() # Cut pipe.
746 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000748 if url:
749 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
750 remotes = RunGit(['config', '--get-regexp',
751 r'^svn-remote\..*\.url']).splitlines()
752 for remote in remotes:
753 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000755 remote = match.group(1)
756 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000757 rewrite_root = RunGit(
758 ['config', 'svn-remote.%s.rewriteRoot' % remote],
759 error_ok=True).strip()
760 if rewrite_root:
761 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000762 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000763 ['config', 'svn-remote.%s.fetch' % remote],
764 error_ok=True).strip()
765 if fetch_spec:
766 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
767 if self.svn_branch:
768 break
769 branch_spec = RunGit(
770 ['config', 'svn-remote.%s.branches' % remote],
771 error_ok=True).strip()
772 if branch_spec:
773 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
774 if self.svn_branch:
775 break
776 tag_spec = RunGit(
777 ['config', 'svn-remote.%s.tags' % remote],
778 error_ok=True).strip()
779 if tag_spec:
780 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
781 if self.svn_branch:
782 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783
784 if not self.svn_branch:
785 DieWithError('Can\'t guess svn branch -- try specifying it on the '
786 'command line')
787
788 return self.svn_branch
789
790 def GetTreeStatusUrl(self, error_ok=False):
791 if not self.tree_status_url:
792 error_message = ('You must configure your tree status URL by running '
793 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self.tree_status_url = self._GetRietveldConfig(
795 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 return self.tree_status_url
797
798 def GetViewVCUrl(self):
799 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 return self.viewvc_url
802
rmistry@google.com90752582014-01-14 21:04:50 +0000803 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000805
rmistry@google.com78948ed2015-07-08 23:09:57 +0000806 def GetIsSkipDependencyUpload(self, branch_name):
807 """Returns true if specified branch should skip dep uploads."""
808 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
809 error_ok=True)
810
rmistry@google.com5626a922015-02-26 14:03:30 +0000811 def GetRunPostUploadHook(self):
812 run_post_upload_hook = self._GetRietveldConfig(
813 'run-post-upload-hook', error_ok=True)
814 return run_post_upload_hook == "True"
815
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000816 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000817 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000818
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000819 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000820 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000821
ukai@chromium.orge8077812012-02-03 03:41:46 +0000822 def GetIsGerrit(self):
823 """Return true if this repo is assosiated with gerrit code review system."""
824 if self.is_gerrit is None:
825 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
826 return self.is_gerrit
827
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000828 def GetSquashGerritUploads(self):
829 """Return true if uploads to Gerrit should be squashed by default."""
830 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700831 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
832 if self.squash_gerrit_uploads is None:
833 # Default is squash now (http://crbug.com/611892#c23).
834 self.squash_gerrit_uploads = not (
835 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
836 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000837 return self.squash_gerrit_uploads
838
tandriia60502f2016-06-20 02:01:53 -0700839 def GetSquashGerritUploadsOverride(self):
840 """Return True or False if codereview.settings should be overridden.
841
842 Returns None if no override has been defined.
843 """
844 # See also http://crbug.com/611892#c23
845 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
846 error_ok=True).strip()
847 if result == 'true':
848 return True
849 if result == 'false':
850 return False
851 return None
852
tandrii@chromium.org28253532016-04-14 13:46:56 +0000853 def GetGerritSkipEnsureAuthenticated(self):
854 """Return True if EnsureAuthenticated should not be done for Gerrit
855 uploads."""
856 if self.gerrit_skip_ensure_authenticated is None:
857 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000858 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000859 error_ok=True).strip() == 'true')
860 return self.gerrit_skip_ensure_authenticated
861
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000862 def GetGitEditor(self):
863 """Return the editor specified in the git config, or None if none is."""
864 if self.git_editor is None:
865 self.git_editor = self._GetConfig('core.editor', error_ok=True)
866 return self.git_editor or None
867
thestig@chromium.org44202a22014-03-11 19:22:18 +0000868 def GetLintRegex(self):
869 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
870 DEFAULT_LINT_REGEX)
871
872 def GetLintIgnoreRegex(self):
873 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
874 DEFAULT_LINT_IGNORE_REGEX)
875
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000876 def GetProject(self):
877 if not self.project:
878 self.project = self._GetRietveldConfig('project', error_ok=True)
879 return self.project
880
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000881 def GetForceHttpsCommitUrl(self):
882 if not self.force_https_commit_url:
883 self.force_https_commit_url = self._GetRietveldConfig(
884 'force-https-commit-url', error_ok=True)
885 return self.force_https_commit_url
886
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000887 def GetPendingRefPrefix(self):
888 if not self.pending_ref_prefix:
889 self.pending_ref_prefix = self._GetRietveldConfig(
890 'pending-ref-prefix', error_ok=True)
891 return self.pending_ref_prefix
892
tandriif46c20f2016-09-14 06:17:05 -0700893 def GetHasGitNumberFooter(self):
894 # TODO(tandrii): this has to be removed after Rietveld is read-only.
895 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
896 if not self.git_number_footer:
897 self.git_number_footer = self._GetRietveldConfig(
898 'git-number-footer', error_ok=True)
899 return self.git_number_footer
900
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000901 def _GetRietveldConfig(self, param, **kwargs):
902 return self._GetConfig('rietveld.' + param, **kwargs)
903
rmistry@google.com78948ed2015-07-08 23:09:57 +0000904 def _GetBranchConfig(self, branch_name, param, **kwargs):
905 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
906
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 def _GetConfig(self, param, **kwargs):
908 self.LazyUpdateIfNeeded()
909 return RunGit(['config', param], **kwargs).strip()
910
911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912def ShortBranchName(branch):
913 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000914 return branch.replace('refs/heads/', '', 1)
915
916
917def GetCurrentBranchRef():
918 """Returns branch ref (e.g., refs/heads/master) or None."""
919 return RunGit(['symbolic-ref', 'HEAD'],
920 stderr=subprocess2.VOID, error_ok=True).strip() or None
921
922
923def GetCurrentBranch():
924 """Returns current branch or None.
925
926 For refs/heads/* branches, returns just last part. For others, full ref.
927 """
928 branchref = GetCurrentBranchRef()
929 if branchref:
930 return ShortBranchName(branchref)
931 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
933
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000934class _CQState(object):
935 """Enum for states of CL with respect to Commit Queue."""
936 NONE = 'none'
937 DRY_RUN = 'dry_run'
938 COMMIT = 'commit'
939
940 ALL_STATES = [NONE, DRY_RUN, COMMIT]
941
942
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000943class _ParsedIssueNumberArgument(object):
944 def __init__(self, issue=None, patchset=None, hostname=None):
945 self.issue = issue
946 self.patchset = patchset
947 self.hostname = hostname
948
949 @property
950 def valid(self):
951 return self.issue is not None
952
953
954class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
955 def __init__(self, *args, **kwargs):
956 self.patch_url = kwargs.pop('patch_url', None)
957 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
958
959
960def ParseIssueNumberArgument(arg):
961 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
962 fail_result = _ParsedIssueNumberArgument()
963
964 if arg.isdigit():
965 return _ParsedIssueNumberArgument(issue=int(arg))
966 if not arg.startswith('http'):
967 return fail_result
968 url = gclient_utils.UpgradeToHttps(arg)
969 try:
970 parsed_url = urlparse.urlparse(url)
971 except ValueError:
972 return fail_result
973 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
974 tmp = cls.ParseIssueURL(parsed_url)
975 if tmp is not None:
976 return tmp
977 return fail_result
978
979
tandriic2405f52016-10-10 08:13:15 -0700980class GerritIssueNotExists(Exception):
981 def __init__(self, issue, url):
982 self.issue = issue
983 self.url = url
984 super(GerritIssueNotExists, self).__init__()
985
986 def __str__(self):
987 return 'issue %s at %s does not exist or you have no access to it' % (
988 self.issue, self.url)
989
990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000992 """Changelist works with one changelist in local branch.
993
994 Supports two codereview backends: Rietveld or Gerrit, selected at object
995 creation.
996
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000997 Notes:
998 * Not safe for concurrent multi-{thread,process} use.
999 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001000 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001001 """
1002
1003 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1004 """Create a new ChangeList instance.
1005
1006 If issue is given, the codereview must be given too.
1007
1008 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1009 Otherwise, it's decided based on current configuration of the local branch,
1010 with default being 'rietveld' for backwards compatibility.
1011 See _load_codereview_impl for more details.
1012
1013 **kwargs will be passed directly to codereview implementation.
1014 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001015 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001016 global settings
1017 if not settings:
1018 # Happens when git_cl.py is used as a utility library.
1019 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001020
1021 if issue:
1022 assert codereview, 'codereview must be known, if issue is known'
1023
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024 self.branchref = branchref
1025 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001026 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 self.branch = ShortBranchName(self.branchref)
1028 else:
1029 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001031 self.lookedup_issue = False
1032 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 self.has_description = False
1034 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001035 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001037 self.cc = None
1038 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001039 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001040
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001041 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001042 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001043 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001044 assert self._codereview_impl
1045 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001046
1047 def _load_codereview_impl(self, codereview=None, **kwargs):
1048 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001049 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1050 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1051 self._codereview = codereview
1052 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001053 return
1054
1055 # Automatic selection based on issue number set for a current branch.
1056 # Rietveld takes precedence over Gerrit.
1057 assert not self.issue
1058 # Whether we find issue or not, we are doing the lookup.
1059 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001060 if self.GetBranch():
1061 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1062 issue = _git_get_branch_config_value(
1063 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1064 if issue:
1065 self._codereview = codereview
1066 self._codereview_impl = cls(self, **kwargs)
1067 self.issue = int(issue)
1068 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001069
1070 # No issue is set for this branch, so decide based on repo-wide settings.
1071 return self._load_codereview_impl(
1072 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1073 **kwargs)
1074
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001075 def IsGerrit(self):
1076 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001077
1078 def GetCCList(self):
1079 """Return the users cc'd on this CL.
1080
agable92bec4f2016-08-24 09:27:27 -07001081 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001082 """
1083 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001084 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001085 more_cc = ','.join(self.watchers)
1086 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1087 return self.cc
1088
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001089 def GetCCListWithoutDefault(self):
1090 """Return the users cc'd on this CL excluding default ones."""
1091 if self.cc is None:
1092 self.cc = ','.join(self.watchers)
1093 return self.cc
1094
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001095 def SetWatchers(self, watchers):
1096 """Set the list of email addresses that should be cc'd based on the changed
1097 files in this CL.
1098 """
1099 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100
1101 def GetBranch(self):
1102 """Returns the short branch name, e.g. 'master'."""
1103 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001104 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001105 if not branchref:
1106 return None
1107 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 self.branch = ShortBranchName(self.branchref)
1109 return self.branch
1110
1111 def GetBranchRef(self):
1112 """Returns the full branch name, e.g. 'refs/heads/master'."""
1113 self.GetBranch() # Poke the lazy loader.
1114 return self.branchref
1115
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001116 def ClearBranch(self):
1117 """Clears cached branch data of this object."""
1118 self.branch = self.branchref = None
1119
tandrii5d48c322016-08-18 16:19:37 -07001120 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1121 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1122 kwargs['branch'] = self.GetBranch()
1123 return _git_get_branch_config_value(key, default, **kwargs)
1124
1125 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1126 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1127 assert self.GetBranch(), (
1128 'this CL must have an associated branch to %sset %s%s' %
1129 ('un' if value is None else '',
1130 key,
1131 '' if value is None else ' to %r' % value))
1132 kwargs['branch'] = self.GetBranch()
1133 return _git_set_branch_config_value(key, value, **kwargs)
1134
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001135 @staticmethod
1136 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001137 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 e.g. 'origin', 'refs/heads/master'
1139 """
1140 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001141 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1142
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001144 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001146 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1147 error_ok=True).strip()
1148 if upstream_branch:
1149 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001151 # Fall back on trying a git-svn upstream branch.
1152 if settings.GetIsGitSvn():
1153 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001155 # Else, try to guess the origin remote.
1156 remote_branches = RunGit(['branch', '-r']).split()
1157 if 'origin/master' in remote_branches:
1158 # Fall back on origin/master if it exits.
1159 remote = 'origin'
1160 upstream_branch = 'refs/heads/master'
1161 elif 'origin/trunk' in remote_branches:
1162 # Fall back on origin/trunk if it exists. Generally a shared
1163 # git-svn clone
1164 remote = 'origin'
1165 upstream_branch = 'refs/heads/trunk'
1166 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001167 DieWithError(
1168 'Unable to determine default branch to diff against.\n'
1169 'Either pass complete "git diff"-style arguments, like\n'
1170 ' git cl upload origin/master\n'
1171 'or verify this branch is set up to track another \n'
1172 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
1174 return remote, upstream_branch
1175
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001176 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001177 upstream_branch = self.GetUpstreamBranch()
1178 if not BranchExists(upstream_branch):
1179 DieWithError('The upstream for the current branch (%s) does not exist '
1180 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001181 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001182 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 def GetUpstreamBranch(self):
1185 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001186 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001188 upstream_branch = upstream_branch.replace('refs/heads/',
1189 'refs/remotes/%s/' % remote)
1190 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1191 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 self.upstream_branch = upstream_branch
1193 return self.upstream_branch
1194
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001195 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001196 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001197 remote, branch = None, self.GetBranch()
1198 seen_branches = set()
1199 while branch not in seen_branches:
1200 seen_branches.add(branch)
1201 remote, branch = self.FetchUpstreamTuple(branch)
1202 branch = ShortBranchName(branch)
1203 if remote != '.' or branch.startswith('refs/remotes'):
1204 break
1205 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001206 remotes = RunGit(['remote'], error_ok=True).split()
1207 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001208 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001209 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001210 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001211 logging.warning('Could not determine which remote this change is '
1212 'associated with, so defaulting to "%s". This may '
1213 'not be what you want. You may prevent this message '
1214 'by running "git svn info" as documented here: %s',
1215 self._remote,
1216 GIT_INSTRUCTIONS_URL)
1217 else:
1218 logging.warn('Could not determine which remote this change is '
1219 'associated with. You may prevent this message by '
1220 'running "git svn info" as documented here: %s',
1221 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001222 branch = 'HEAD'
1223 if branch.startswith('refs/remotes'):
1224 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001225 elif branch.startswith('refs/branch-heads/'):
1226 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001227 else:
1228 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001229 return self._remote
1230
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001231 def GitSanityChecks(self, upstream_git_obj):
1232 """Checks git repo status and ensures diff is from local commits."""
1233
sbc@chromium.org79706062015-01-14 21:18:12 +00001234 if upstream_git_obj is None:
1235 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001236 print('ERROR: unable to determine current branch (detached HEAD?)',
1237 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001238 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001239 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001240 return False
1241
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001242 # Verify the commit we're diffing against is in our current branch.
1243 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1244 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1245 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001246 print('ERROR: %s is not in the current branch. You may need to rebase '
1247 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001248 return False
1249
1250 # List the commits inside the diff, and verify they are all local.
1251 commits_in_diff = RunGit(
1252 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1253 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1254 remote_branch = remote_branch.strip()
1255 if code != 0:
1256 _, remote_branch = self.GetRemoteBranch()
1257
1258 commits_in_remote = RunGit(
1259 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1260
1261 common_commits = set(commits_in_diff) & set(commits_in_remote)
1262 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001263 print('ERROR: Your diff contains %d commits already in %s.\n'
1264 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1265 'the diff. If you are using a custom git flow, you can override'
1266 ' the reference used for this check with "git config '
1267 'gitcl.remotebranch <git-ref>".' % (
1268 len(common_commits), remote_branch, upstream_git_obj),
1269 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 return False
1271 return True
1272
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001273 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001274 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001275
1276 Returns None if it is not set.
1277 """
tandrii5d48c322016-08-18 16:19:37 -07001278 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001279
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001280 def GetGitSvnRemoteUrl(self):
1281 """Return the configured git-svn remote URL parsed from git svn info.
1282
1283 Returns None if it is not set.
1284 """
1285 # URL is dependent on the current directory.
1286 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1287 if data:
1288 keys = dict(line.split(': ', 1) for line in data.splitlines()
1289 if ': ' in line)
1290 return keys.get('URL', None)
1291 return None
1292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 def GetRemoteUrl(self):
1294 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1295
1296 Returns None if there is no remote.
1297 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001299 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1300
1301 # If URL is pointing to a local directory, it is probably a git cache.
1302 if os.path.isdir(url):
1303 url = RunGit(['config', 'remote.%s.url' % remote],
1304 error_ok=True,
1305 cwd=url).strip()
1306 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001308 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001309 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001310 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001311 self.issue = self._GitGetBranchConfigValue(
1312 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001313 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 return self.issue
1315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 def GetIssueURL(self):
1317 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001318 issue = self.GetIssue()
1319 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001320 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001321 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323 def GetDescription(self, pretty=False):
1324 if not self.has_description:
1325 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001326 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 self.has_description = True
1328 if pretty:
1329 wrapper = textwrap.TextWrapper()
1330 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1331 return wrapper.fill(self.description)
1332 return self.description
1333
1334 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001335 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001336 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001337 self.patchset = self._GitGetBranchConfigValue(
1338 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001339 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 return self.patchset
1341
1342 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001343 """Set this branch's patchset. If patchset=0, clears the patchset."""
1344 assert self.GetBranch()
1345 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001346 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001347 else:
1348 self.patchset = int(patchset)
1349 self._GitSetBranchConfigValue(
1350 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001352 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001353 """Set this branch's issue. If issue isn't given, clears the issue."""
1354 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001356 issue = int(issue)
1357 self._GitSetBranchConfigValue(
1358 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001359 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001360 codereview_server = self._codereview_impl.GetCodereviewServer()
1361 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001362 self._GitSetBranchConfigValue(
1363 self._codereview_impl.CodereviewServerConfigKey(),
1364 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 else:
tandrii5d48c322016-08-18 16:19:37 -07001366 # Reset all of these just to be clean.
1367 reset_suffixes = [
1368 'last-upload-hash',
1369 self._codereview_impl.IssueConfigKey(),
1370 self._codereview_impl.PatchsetConfigKey(),
1371 self._codereview_impl.CodereviewServerConfigKey(),
1372 ] + self._PostUnsetIssueProperties()
1373 for prop in reset_suffixes:
1374 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001375 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001376 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377
dnjba1b0f32016-09-02 12:37:42 -07001378 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001379 if not self.GitSanityChecks(upstream_branch):
1380 DieWithError('\nGit sanity check failure')
1381
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001382 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001383 if not root:
1384 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001385 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001386
1387 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001388 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001389 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001390 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001391 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001392 except subprocess2.CalledProcessError:
1393 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001394 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001395 'This branch probably doesn\'t exist anymore. To reset the\n'
1396 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001397 ' git branch --set-upstream-to origin/master %s\n'
1398 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001399 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001400
maruel@chromium.org52424302012-08-29 15:14:30 +00001401 issue = self.GetIssue()
1402 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001403 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001404 description = self.GetDescription()
1405 else:
1406 # If the change was never uploaded, use the log messages of all commits
1407 # up to the branch point, as git cl upload will prefill the description
1408 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001409 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1410 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001411
1412 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001413 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001414 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001415 name,
1416 description,
1417 absroot,
1418 files,
1419 issue,
1420 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001421 author,
1422 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001423
dsansomee2d6fd92016-09-08 00:10:47 -07001424 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001425 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001426 return self._codereview_impl.UpdateDescriptionRemote(
1427 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001428
1429 def RunHook(self, committing, may_prompt, verbose, change):
1430 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1431 try:
1432 return presubmit_support.DoPresubmitChecks(change, committing,
1433 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1434 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001435 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1436 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001437 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001438 DieWithError(
1439 ('%s\nMaybe your depot_tools is out of date?\n'
1440 'If all fails, contact maruel@') % e)
1441
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001442 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1443 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001444 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1445 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001446 else:
1447 # Assume url.
1448 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1449 urlparse.urlparse(issue_arg))
1450 if not parsed_issue_arg or not parsed_issue_arg.valid:
1451 DieWithError('Failed to parse issue argument "%s". '
1452 'Must be an issue number or a valid URL.' % issue_arg)
1453 return self._codereview_impl.CMDPatchWithParsedIssue(
1454 parsed_issue_arg, reject, nocommit, directory)
1455
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001456 def CMDUpload(self, options, git_diff_args, orig_args):
1457 """Uploads a change to codereview."""
1458 if git_diff_args:
1459 # TODO(ukai): is it ok for gerrit case?
1460 base_branch = git_diff_args[0]
1461 else:
1462 if self.GetBranch() is None:
1463 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1464
1465 # Default to diffing against common ancestor of upstream branch
1466 base_branch = self.GetCommonAncestorWithUpstream()
1467 git_diff_args = [base_branch, 'HEAD']
1468
1469 # Make sure authenticated to codereview before running potentially expensive
1470 # hooks. It is a fast, best efforts check. Codereview still can reject the
1471 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001472 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001473
1474 # Apply watchlists on upload.
1475 change = self.GetChange(base_branch, None)
1476 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1477 files = [f.LocalPath() for f in change.AffectedFiles()]
1478 if not options.bypass_watchlists:
1479 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1480
1481 if not options.bypass_hooks:
1482 if options.reviewers or options.tbr_owners:
1483 # Set the reviewer list now so that presubmit checks can access it.
1484 change_description = ChangeDescription(change.FullDescriptionText())
1485 change_description.update_reviewers(options.reviewers,
1486 options.tbr_owners,
1487 change)
1488 change.SetDescriptionText(change_description.description)
1489 hook_results = self.RunHook(committing=False,
1490 may_prompt=not options.force,
1491 verbose=options.verbose,
1492 change=change)
1493 if not hook_results.should_continue():
1494 return 1
1495 if not options.reviewers and hook_results.reviewers:
1496 options.reviewers = hook_results.reviewers.split(',')
1497
1498 if self.GetIssue():
1499 latest_patchset = self.GetMostRecentPatchset()
1500 local_patchset = self.GetPatchset()
1501 if (latest_patchset and local_patchset and
1502 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001503 print('The last upload made from this repository was patchset #%d but '
1504 'the most recent patchset on the server is #%d.'
1505 % (local_patchset, latest_patchset))
1506 print('Uploading will still work, but if you\'ve uploaded to this '
1507 'issue from another machine or branch the patch you\'re '
1508 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001509 ask_for_data('About to upload; enter to confirm.')
1510
1511 print_stats(options.similarity, options.find_copies, git_diff_args)
1512 ret = self.CMDUploadChange(options, git_diff_args, change)
1513 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001514 if options.use_commit_queue:
1515 self.SetCQState(_CQState.COMMIT)
1516 elif options.cq_dry_run:
1517 self.SetCQState(_CQState.DRY_RUN)
1518
tandrii5d48c322016-08-18 16:19:37 -07001519 _git_set_branch_config_value('last-upload-hash',
1520 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001521 # Run post upload hooks, if specified.
1522 if settings.GetRunPostUploadHook():
1523 presubmit_support.DoPostUploadExecuter(
1524 change,
1525 self,
1526 settings.GetRoot(),
1527 options.verbose,
1528 sys.stdout)
1529
1530 # Upload all dependencies if specified.
1531 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001532 print()
1533 print('--dependencies has been specified.')
1534 print('All dependent local branches will be re-uploaded.')
1535 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001536 # Remove the dependencies flag from args so that we do not end up in a
1537 # loop.
1538 orig_args.remove('--dependencies')
1539 ret = upload_branch_deps(self, orig_args)
1540 return ret
1541
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001542 def SetCQState(self, new_state):
1543 """Update the CQ state for latest patchset.
1544
1545 Issue must have been already uploaded and known.
1546 """
1547 assert new_state in _CQState.ALL_STATES
1548 assert self.GetIssue()
1549 return self._codereview_impl.SetCQState(new_state)
1550
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001551 # Forward methods to codereview specific implementation.
1552
1553 def CloseIssue(self):
1554 return self._codereview_impl.CloseIssue()
1555
1556 def GetStatus(self):
1557 return self._codereview_impl.GetStatus()
1558
1559 def GetCodereviewServer(self):
1560 return self._codereview_impl.GetCodereviewServer()
1561
1562 def GetApprovingReviewers(self):
1563 return self._codereview_impl.GetApprovingReviewers()
1564
1565 def GetMostRecentPatchset(self):
1566 return self._codereview_impl.GetMostRecentPatchset()
1567
1568 def __getattr__(self, attr):
1569 # This is because lots of untested code accesses Rietveld-specific stuff
1570 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001571 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001572 # Note that child method defines __getattr__ as well, and forwards it here,
1573 # because _RietveldChangelistImpl is not cleaned up yet, and given
1574 # deprecation of Rietveld, it should probably be just removed.
1575 # Until that time, avoid infinite recursion by bypassing __getattr__
1576 # of implementation class.
1577 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001578
1579
1580class _ChangelistCodereviewBase(object):
1581 """Abstract base class encapsulating codereview specifics of a changelist."""
1582 def __init__(self, changelist):
1583 self._changelist = changelist # instance of Changelist
1584
1585 def __getattr__(self, attr):
1586 # Forward methods to changelist.
1587 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1588 # _RietveldChangelistImpl to avoid this hack?
1589 return getattr(self._changelist, attr)
1590
1591 def GetStatus(self):
1592 """Apply a rough heuristic to give a simple summary of an issue's review
1593 or CQ status, assuming adherence to a common workflow.
1594
1595 Returns None if no issue for this branch, or specific string keywords.
1596 """
1597 raise NotImplementedError()
1598
1599 def GetCodereviewServer(self):
1600 """Returns server URL without end slash, like "https://codereview.com"."""
1601 raise NotImplementedError()
1602
1603 def FetchDescription(self):
1604 """Fetches and returns description from the codereview server."""
1605 raise NotImplementedError()
1606
tandrii5d48c322016-08-18 16:19:37 -07001607 @classmethod
1608 def IssueConfigKey(cls):
1609 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001610 raise NotImplementedError()
1611
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001612 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001613 def PatchsetConfigKey(cls):
1614 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001615 raise NotImplementedError()
1616
tandrii5d48c322016-08-18 16:19:37 -07001617 @classmethod
1618 def CodereviewServerConfigKey(cls):
1619 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001620 raise NotImplementedError()
1621
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001622 def _PostUnsetIssueProperties(self):
1623 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001624 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001625
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001626 def GetRieveldObjForPresubmit(self):
1627 # This is an unfortunate Rietveld-embeddedness in presubmit.
1628 # For non-Rietveld codereviews, this probably should return a dummy object.
1629 raise NotImplementedError()
1630
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001631 def GetGerritObjForPresubmit(self):
1632 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1633 return None
1634
dsansomee2d6fd92016-09-08 00:10:47 -07001635 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001636 """Update the description on codereview site."""
1637 raise NotImplementedError()
1638
1639 def CloseIssue(self):
1640 """Closes the issue."""
1641 raise NotImplementedError()
1642
1643 def GetApprovingReviewers(self):
1644 """Returns a list of reviewers approving the change.
1645
1646 Note: not necessarily committers.
1647 """
1648 raise NotImplementedError()
1649
1650 def GetMostRecentPatchset(self):
1651 """Returns the most recent patchset number from the codereview site."""
1652 raise NotImplementedError()
1653
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001654 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1655 directory):
1656 """Fetches and applies the issue.
1657
1658 Arguments:
1659 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1660 reject: if True, reject the failed patch instead of switching to 3-way
1661 merge. Rietveld only.
1662 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1663 only.
1664 directory: switch to directory before applying the patch. Rietveld only.
1665 """
1666 raise NotImplementedError()
1667
1668 @staticmethod
1669 def ParseIssueURL(parsed_url):
1670 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1671 failed."""
1672 raise NotImplementedError()
1673
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001674 def EnsureAuthenticated(self, force):
1675 """Best effort check that user is authenticated with codereview server.
1676
1677 Arguments:
1678 force: whether to skip confirmation questions.
1679 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001680 raise NotImplementedError()
1681
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001682 def CMDUploadChange(self, options, args, change):
1683 """Uploads a change to codereview."""
1684 raise NotImplementedError()
1685
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001686 def SetCQState(self, new_state):
1687 """Update the CQ state for latest patchset.
1688
1689 Issue must have been already uploaded and known.
1690 """
1691 raise NotImplementedError()
1692
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001693
1694class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1695 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1696 super(_RietveldChangelistImpl, self).__init__(changelist)
1697 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001698 if not rietveld_server:
1699 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001700
1701 self._rietveld_server = rietveld_server
1702 self._auth_config = auth_config
1703 self._props = None
1704 self._rpc_server = None
1705
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001706 def GetCodereviewServer(self):
1707 if not self._rietveld_server:
1708 # If we're on a branch then get the server potentially associated
1709 # with that branch.
1710 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001711 self._rietveld_server = gclient_utils.UpgradeToHttps(
1712 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001713 if not self._rietveld_server:
1714 self._rietveld_server = settings.GetDefaultServerUrl()
1715 return self._rietveld_server
1716
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001717 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001718 """Best effort check that user is authenticated with Rietveld server."""
1719 if self._auth_config.use_oauth2:
1720 authenticator = auth.get_authenticator_for_host(
1721 self.GetCodereviewServer(), self._auth_config)
1722 if not authenticator.has_cached_credentials():
1723 raise auth.LoginRequiredError(self.GetCodereviewServer())
1724
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 def FetchDescription(self):
1726 issue = self.GetIssue()
1727 assert issue
1728 try:
1729 return self.RpcServer().get_description(issue).strip()
1730 except urllib2.HTTPError as e:
1731 if e.code == 404:
1732 DieWithError(
1733 ('\nWhile fetching the description for issue %d, received a '
1734 '404 (not found)\n'
1735 'error. It is likely that you deleted this '
1736 'issue on the server. If this is the\n'
1737 'case, please run\n\n'
1738 ' git cl issue 0\n\n'
1739 'to clear the association with the deleted issue. Then run '
1740 'this command again.') % issue)
1741 else:
1742 DieWithError(
1743 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1744 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001745 print('Warning: Failed to retrieve CL description due to network '
1746 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001747 return ''
1748
1749 def GetMostRecentPatchset(self):
1750 return self.GetIssueProperties()['patchsets'][-1]
1751
1752 def GetPatchSetDiff(self, issue, patchset):
1753 return self.RpcServer().get(
1754 '/download/issue%s_%s.diff' % (issue, patchset))
1755
1756 def GetIssueProperties(self):
1757 if self._props is None:
1758 issue = self.GetIssue()
1759 if not issue:
1760 self._props = {}
1761 else:
1762 self._props = self.RpcServer().get_issue_properties(issue, True)
1763 return self._props
1764
1765 def GetApprovingReviewers(self):
1766 return get_approving_reviewers(self.GetIssueProperties())
1767
1768 def AddComment(self, message):
1769 return self.RpcServer().add_comment(self.GetIssue(), message)
1770
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001771 def GetStatus(self):
1772 """Apply a rough heuristic to give a simple summary of an issue's review
1773 or CQ status, assuming adherence to a common workflow.
1774
1775 Returns None if no issue for this branch, or one of the following keywords:
1776 * 'error' - error from review tool (including deleted issues)
1777 * 'unsent' - not sent for review
1778 * 'waiting' - waiting for review
1779 * 'reply' - waiting for owner to reply to review
1780 * 'lgtm' - LGTM from at least one approved reviewer
1781 * 'commit' - in the commit queue
1782 * 'closed' - closed
1783 """
1784 if not self.GetIssue():
1785 return None
1786
1787 try:
1788 props = self.GetIssueProperties()
1789 except urllib2.HTTPError:
1790 return 'error'
1791
1792 if props.get('closed'):
1793 # Issue is closed.
1794 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001795 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001796 # Issue is in the commit queue.
1797 return 'commit'
1798
1799 try:
1800 reviewers = self.GetApprovingReviewers()
1801 except urllib2.HTTPError:
1802 return 'error'
1803
1804 if reviewers:
1805 # Was LGTM'ed.
1806 return 'lgtm'
1807
1808 messages = props.get('messages') or []
1809
tandrii9d2c7a32016-06-22 03:42:45 -07001810 # Skip CQ messages that don't require owner's action.
1811 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1812 if 'Dry run:' in messages[-1]['text']:
1813 messages.pop()
1814 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1815 # This message always follows prior messages from CQ,
1816 # so skip this too.
1817 messages.pop()
1818 else:
1819 # This is probably a CQ messages warranting user attention.
1820 break
1821
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001822 if not messages:
1823 # No message was sent.
1824 return 'unsent'
1825 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001826 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001827 return 'reply'
1828 return 'waiting'
1829
dsansomee2d6fd92016-09-08 00:10:47 -07001830 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001831 return self.RpcServer().update_description(
1832 self.GetIssue(), self.description)
1833
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001835 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001837 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001838 return self.SetFlags({flag: value})
1839
1840 def SetFlags(self, flags):
1841 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001842 """
phajdan.jr68598232016-08-10 03:28:28 -07001843 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001844 try:
tandrii4b233bd2016-07-06 03:50:29 -07001845 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001846 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001847 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001848 if e.code == 404:
1849 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1850 if e.code == 403:
1851 DieWithError(
1852 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001853 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001854 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001856 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001857 """Returns an upload.RpcServer() to access this review's rietveld instance.
1858 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001859 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001860 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001861 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001862 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001863 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001865 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001866 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001867 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001868
tandrii5d48c322016-08-18 16:19:37 -07001869 @classmethod
1870 def PatchsetConfigKey(cls):
1871 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872
tandrii5d48c322016-08-18 16:19:37 -07001873 @classmethod
1874 def CodereviewServerConfigKey(cls):
1875 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001876
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001877 def GetRieveldObjForPresubmit(self):
1878 return self.RpcServer()
1879
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001880 def SetCQState(self, new_state):
1881 props = self.GetIssueProperties()
1882 if props.get('private'):
1883 DieWithError('Cannot set-commit on private issue')
1884
1885 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001886 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001887 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001888 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001889 else:
tandrii4b233bd2016-07-06 03:50:29 -07001890 assert new_state == _CQState.DRY_RUN
1891 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001892
1893
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001894 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1895 directory):
1896 # TODO(maruel): Use apply_issue.py
1897
1898 # PatchIssue should never be called with a dirty tree. It is up to the
1899 # caller to check this, but just in case we assert here since the
1900 # consequences of the caller not checking this could be dire.
1901 assert(not git_common.is_dirty_git_tree('apply'))
1902 assert(parsed_issue_arg.valid)
1903 self._changelist.issue = parsed_issue_arg.issue
1904 if parsed_issue_arg.hostname:
1905 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1906
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001907 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1908 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001909 assert parsed_issue_arg.patchset
1910 patchset = parsed_issue_arg.patchset
1911 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1912 else:
1913 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1914 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1915
1916 # Switch up to the top-level directory, if necessary, in preparation for
1917 # applying the patch.
1918 top = settings.GetRelativeRoot()
1919 if top:
1920 os.chdir(top)
1921
1922 # Git patches have a/ at the beginning of source paths. We strip that out
1923 # with a sed script rather than the -p flag to patch so we can feed either
1924 # Git or svn-style patches into the same apply command.
1925 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1926 try:
1927 patch_data = subprocess2.check_output(
1928 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1929 except subprocess2.CalledProcessError:
1930 DieWithError('Git patch mungling failed.')
1931 logging.info(patch_data)
1932
1933 # We use "git apply" to apply the patch instead of "patch" so that we can
1934 # pick up file adds.
1935 # The --index flag means: also insert into the index (so we catch adds).
1936 cmd = ['git', 'apply', '--index', '-p0']
1937 if directory:
1938 cmd.extend(('--directory', directory))
1939 if reject:
1940 cmd.append('--reject')
1941 elif IsGitVersionAtLeast('1.7.12'):
1942 cmd.append('--3way')
1943 try:
1944 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1945 stdin=patch_data, stdout=subprocess2.VOID)
1946 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001947 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001948 return 1
1949
1950 # If we had an issue, commit the current state and register the issue.
1951 if not nocommit:
1952 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1953 'patch from issue %(i)s at patchset '
1954 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1955 % {'i': self.GetIssue(), 'p': patchset})])
1956 self.SetIssue(self.GetIssue())
1957 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001958 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001959 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001960 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001961 return 0
1962
1963 @staticmethod
1964 def ParseIssueURL(parsed_url):
1965 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1966 return None
wychen3c1c1722016-08-04 11:46:36 -07001967 # Rietveld patch: https://domain/<number>/#ps<patchset>
1968 match = re.match(r'/(\d+)/$', parsed_url.path)
1969 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1970 if match and match2:
1971 return _RietveldParsedIssueNumberArgument(
1972 issue=int(match.group(1)),
1973 patchset=int(match2.group(1)),
1974 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001975 # Typical url: https://domain/<issue_number>[/[other]]
1976 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1977 if match:
1978 return _RietveldParsedIssueNumberArgument(
1979 issue=int(match.group(1)),
1980 hostname=parsed_url.netloc)
1981 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1982 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1983 if match:
1984 return _RietveldParsedIssueNumberArgument(
1985 issue=int(match.group(1)),
1986 patchset=int(match.group(2)),
1987 hostname=parsed_url.netloc,
1988 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1989 return None
1990
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001991 def CMDUploadChange(self, options, args, change):
1992 """Upload the patch to Rietveld."""
1993 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1994 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001995 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1996 if options.emulate_svn_auto_props:
1997 upload_args.append('--emulate_svn_auto_props')
1998
1999 change_desc = None
2000
2001 if options.email is not None:
2002 upload_args.extend(['--email', options.email])
2003
2004 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002005 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002006 upload_args.extend(['--title', options.title])
2007 if options.message:
2008 upload_args.extend(['--message', options.message])
2009 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002010 print('This branch is associated with issue %s. '
2011 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002012 else:
nodirca166002016-06-27 10:59:51 -07002013 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002014 upload_args.extend(['--title', options.title])
2015 message = (options.title or options.message or
2016 CreateDescriptionFromLog(args))
2017 change_desc = ChangeDescription(message)
2018 if options.reviewers or options.tbr_owners:
2019 change_desc.update_reviewers(options.reviewers,
2020 options.tbr_owners,
2021 change)
2022 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002023 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002024
2025 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002026 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002027 return 1
2028
2029 upload_args.extend(['--message', change_desc.description])
2030 if change_desc.get_reviewers():
2031 upload_args.append('--reviewers=%s' % ','.join(
2032 change_desc.get_reviewers()))
2033 if options.send_mail:
2034 if not change_desc.get_reviewers():
2035 DieWithError("Must specify reviewers to send email.")
2036 upload_args.append('--send_mail')
2037
2038 # We check this before applying rietveld.private assuming that in
2039 # rietveld.cc only addresses which we can send private CLs to are listed
2040 # if rietveld.private is set, and so we should ignore rietveld.cc only
2041 # when --private is specified explicitly on the command line.
2042 if options.private:
2043 logging.warn('rietveld.cc is ignored since private flag is specified. '
2044 'You need to review and add them manually if necessary.')
2045 cc = self.GetCCListWithoutDefault()
2046 else:
2047 cc = self.GetCCList()
2048 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
2049 if cc:
2050 upload_args.extend(['--cc', cc])
2051
2052 if options.private or settings.GetDefaultPrivateFlag() == "True":
2053 upload_args.append('--private')
2054
2055 upload_args.extend(['--git_similarity', str(options.similarity)])
2056 if not options.find_copies:
2057 upload_args.extend(['--git_no_find_copies'])
2058
2059 # Include the upstream repo's URL in the change -- this is useful for
2060 # projects that have their source spread across multiple repos.
2061 remote_url = self.GetGitBaseUrlFromConfig()
2062 if not remote_url:
2063 if settings.GetIsGitSvn():
2064 remote_url = self.GetGitSvnRemoteUrl()
2065 else:
2066 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2067 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2068 self.GetUpstreamBranch().split('/')[-1])
2069 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002070 remote, remote_branch = self.GetRemoteBranch()
2071 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2072 settings.GetPendingRefPrefix())
2073 if target_ref:
2074 upload_args.extend(['--target_ref', target_ref])
2075
2076 # Look for dependent patchsets. See crbug.com/480453 for more details.
2077 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2078 upstream_branch = ShortBranchName(upstream_branch)
2079 if remote is '.':
2080 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002081 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002082 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002083 print()
2084 print('Skipping dependency patchset upload because git config '
2085 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2086 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002087 else:
2088 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002089 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002090 auth_config=auth_config)
2091 branch_cl_issue_url = branch_cl.GetIssueURL()
2092 branch_cl_issue = branch_cl.GetIssue()
2093 branch_cl_patchset = branch_cl.GetPatchset()
2094 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2095 upload_args.extend(
2096 ['--depends_on_patchset', '%s:%s' % (
2097 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002098 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002099 '\n'
2100 'The current branch (%s) is tracking a local branch (%s) with '
2101 'an associated CL.\n'
2102 'Adding %s/#ps%s as a dependency patchset.\n'
2103 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2104 branch_cl_patchset))
2105
2106 project = settings.GetProject()
2107 if project:
2108 upload_args.extend(['--project', project])
2109
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002110 try:
2111 upload_args = ['upload'] + upload_args + args
2112 logging.info('upload.RealMain(%s)', upload_args)
2113 issue, patchset = upload.RealMain(upload_args)
2114 issue = int(issue)
2115 patchset = int(patchset)
2116 except KeyboardInterrupt:
2117 sys.exit(1)
2118 except:
2119 # If we got an exception after the user typed a description for their
2120 # change, back up the description before re-raising.
2121 if change_desc:
2122 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2123 print('\nGot exception while uploading -- saving description to %s\n' %
2124 backup_path)
2125 backup_file = open(backup_path, 'w')
2126 backup_file.write(change_desc.description)
2127 backup_file.close()
2128 raise
2129
2130 if not self.GetIssue():
2131 self.SetIssue(issue)
2132 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002133 return 0
2134
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002135
2136class _GerritChangelistImpl(_ChangelistCodereviewBase):
2137 def __init__(self, changelist, auth_config=None):
2138 # auth_config is Rietveld thing, kept here to preserve interface only.
2139 super(_GerritChangelistImpl, self).__init__(changelist)
2140 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002141 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002142 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002143 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002144
2145 def _GetGerritHost(self):
2146 # Lazy load of configs.
2147 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002148 if self._gerrit_host and '.' not in self._gerrit_host:
2149 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2150 # This happens for internal stuff http://crbug.com/614312.
2151 parsed = urlparse.urlparse(self.GetRemoteUrl())
2152 if parsed.scheme == 'sso':
2153 print('WARNING: using non https URLs for remote is likely broken\n'
2154 ' Your current remote is: %s' % self.GetRemoteUrl())
2155 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2156 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002157 return self._gerrit_host
2158
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002159 def _GetGitHost(self):
2160 """Returns git host to be used when uploading change to Gerrit."""
2161 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2162
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002163 def GetCodereviewServer(self):
2164 if not self._gerrit_server:
2165 # If we're on a branch then get the server potentially associated
2166 # with that branch.
2167 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002168 self._gerrit_server = self._GitGetBranchConfigValue(
2169 self.CodereviewServerConfigKey())
2170 if self._gerrit_server:
2171 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002172 if not self._gerrit_server:
2173 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2174 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002175 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002176 parts[0] = parts[0] + '-review'
2177 self._gerrit_host = '.'.join(parts)
2178 self._gerrit_server = 'https://%s' % self._gerrit_host
2179 return self._gerrit_server
2180
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002181 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002182 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002183 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002184
tandrii5d48c322016-08-18 16:19:37 -07002185 @classmethod
2186 def PatchsetConfigKey(cls):
2187 return 'gerritpatchset'
2188
2189 @classmethod
2190 def CodereviewServerConfigKey(cls):
2191 return 'gerritserver'
2192
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002193 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002194 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002195 if settings.GetGerritSkipEnsureAuthenticated():
2196 # For projects with unusual authentication schemes.
2197 # See http://crbug.com/603378.
2198 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002199 # Lazy-loader to identify Gerrit and Git hosts.
2200 if gerrit_util.GceAuthenticator.is_gce():
2201 return
2202 self.GetCodereviewServer()
2203 git_host = self._GetGitHost()
2204 assert self._gerrit_server and self._gerrit_host
2205 cookie_auth = gerrit_util.CookiesAuthenticator()
2206
2207 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2208 git_auth = cookie_auth.get_auth_header(git_host)
2209 if gerrit_auth and git_auth:
2210 if gerrit_auth == git_auth:
2211 return
2212 print((
2213 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2214 ' Check your %s or %s file for credentials of hosts:\n'
2215 ' %s\n'
2216 ' %s\n'
2217 ' %s') %
2218 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2219 git_host, self._gerrit_host,
2220 cookie_auth.get_new_password_message(git_host)))
2221 if not force:
2222 ask_for_data('If you know what you are doing, press Enter to continue, '
2223 'Ctrl+C to abort.')
2224 return
2225 else:
2226 missing = (
2227 [] if gerrit_auth else [self._gerrit_host] +
2228 [] if git_auth else [git_host])
2229 DieWithError('Credentials for the following hosts are required:\n'
2230 ' %s\n'
2231 'These are read from %s (or legacy %s)\n'
2232 '%s' % (
2233 '\n '.join(missing),
2234 cookie_auth.get_gitcookies_path(),
2235 cookie_auth.get_netrc_path(),
2236 cookie_auth.get_new_password_message(git_host)))
2237
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002238 def _PostUnsetIssueProperties(self):
2239 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002240 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002241
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002242 def GetRieveldObjForPresubmit(self):
2243 class ThisIsNotRietveldIssue(object):
2244 def __nonzero__(self):
2245 # This is a hack to make presubmit_support think that rietveld is not
2246 # defined, yet still ensure that calls directly result in a decent
2247 # exception message below.
2248 return False
2249
2250 def __getattr__(self, attr):
2251 print(
2252 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2253 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2254 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2255 'or use Rietveld for codereview.\n'
2256 'See also http://crbug.com/579160.' % attr)
2257 raise NotImplementedError()
2258 return ThisIsNotRietveldIssue()
2259
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002260 def GetGerritObjForPresubmit(self):
2261 return presubmit_support.GerritAccessor(self._GetGerritHost())
2262
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002263 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002264 """Apply a rough heuristic to give a simple summary of an issue's review
2265 or CQ status, assuming adherence to a common workflow.
2266
2267 Returns None if no issue for this branch, or one of the following keywords:
2268 * 'error' - error from review tool (including deleted issues)
2269 * 'unsent' - no reviewers added
2270 * 'waiting' - waiting for review
2271 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002272 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2273 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002274 * 'commit' - in the commit queue
2275 * 'closed' - abandoned
2276 """
2277 if not self.GetIssue():
2278 return None
2279
2280 try:
2281 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002282 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002283 return 'error'
2284
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002285 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002286 return 'closed'
2287
2288 cq_label = data['labels'].get('Commit-Queue', {})
2289 if cq_label:
2290 # Vote value is a stringified integer, which we expect from 0 to 2.
2291 vote_value = cq_label.get('value', '0')
2292 vote_text = cq_label.get('values', {}).get(vote_value, '')
2293 if vote_text.lower() == 'commit':
2294 return 'commit'
2295
2296 lgtm_label = data['labels'].get('Code-Review', {})
2297 if lgtm_label:
2298 if 'rejected' in lgtm_label:
2299 return 'not lgtm'
2300 if 'approved' in lgtm_label:
2301 return 'lgtm'
2302
2303 if not data.get('reviewers', {}).get('REVIEWER', []):
2304 return 'unsent'
2305
2306 messages = data.get('messages', [])
2307 if messages:
2308 owner = data['owner'].get('_account_id')
2309 last_message_author = messages[-1].get('author', {}).get('_account_id')
2310 if owner != last_message_author:
2311 # Some reply from non-owner.
2312 return 'reply'
2313
2314 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002315
2316 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002317 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002318 return data['revisions'][data['current_revision']]['_number']
2319
2320 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002321 data = self._GetChangeDetail(['CURRENT_REVISION'])
2322 current_rev = data['current_revision']
2323 url = data['revisions'][current_rev]['fetch']['http']['url']
2324 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002325
dsansomee2d6fd92016-09-08 00:10:47 -07002326 def UpdateDescriptionRemote(self, description, force=False):
2327 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2328 if not force:
2329 ask_for_data(
2330 'The description cannot be modified while the issue has a pending '
2331 'unpublished edit. Either publish the edit in the Gerrit web UI '
2332 'or delete it.\n\n'
2333 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2334
2335 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2336 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002337 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2338 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002339
2340 def CloseIssue(self):
2341 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2342
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002343 def GetApprovingReviewers(self):
2344 """Returns a list of reviewers approving the change.
2345
2346 Note: not necessarily committers.
2347 """
2348 raise NotImplementedError()
2349
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002350 def SubmitIssue(self, wait_for_merge=True):
2351 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2352 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002353
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002354 def _GetChangeDetail(self, options=None, issue=None):
2355 options = options or []
2356 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002357 assert issue, 'issue is required to query Gerrit'
2358 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002359 options)
tandriic2405f52016-10-10 08:13:15 -07002360 if not data:
2361 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2362 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002363
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002364 def CMDLand(self, force, bypass_hooks, verbose):
2365 if git_common.is_dirty_git_tree('land'):
2366 return 1
tandriid60367b2016-06-22 05:25:12 -07002367 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2368 if u'Commit-Queue' in detail.get('labels', {}):
2369 if not force:
2370 ask_for_data('\nIt seems this repository has a Commit Queue, '
2371 'which can test and land changes for you. '
2372 'Are you sure you wish to bypass it?\n'
2373 'Press Enter to continue, Ctrl+C to abort.')
2374
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002375 differs = True
tandriic4344b52016-08-29 06:04:54 -07002376 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002377 # Note: git diff outputs nothing if there is no diff.
2378 if not last_upload or RunGit(['diff', last_upload]).strip():
2379 print('WARNING: some changes from local branch haven\'t been uploaded')
2380 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002381 if detail['current_revision'] == last_upload:
2382 differs = False
2383 else:
2384 print('WARNING: local branch contents differ from latest uploaded '
2385 'patchset')
2386 if differs:
2387 if not force:
2388 ask_for_data(
2389 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2390 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2391 elif not bypass_hooks:
2392 hook_results = self.RunHook(
2393 committing=True,
2394 may_prompt=not force,
2395 verbose=verbose,
2396 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2397 if not hook_results.should_continue():
2398 return 1
2399
2400 self.SubmitIssue(wait_for_merge=True)
2401 print('Issue %s has been submitted.' % self.GetIssueURL())
2402 return 0
2403
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002404 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2405 directory):
2406 assert not reject
2407 assert not nocommit
2408 assert not directory
2409 assert parsed_issue_arg.valid
2410
2411 self._changelist.issue = parsed_issue_arg.issue
2412
2413 if parsed_issue_arg.hostname:
2414 self._gerrit_host = parsed_issue_arg.hostname
2415 self._gerrit_server = 'https://%s' % self._gerrit_host
2416
tandriic2405f52016-10-10 08:13:15 -07002417 try:
2418 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2419 except GerritIssueNotExists as e:
2420 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002421
2422 if not parsed_issue_arg.patchset:
2423 # Use current revision by default.
2424 revision_info = detail['revisions'][detail['current_revision']]
2425 patchset = int(revision_info['_number'])
2426 else:
2427 patchset = parsed_issue_arg.patchset
2428 for revision_info in detail['revisions'].itervalues():
2429 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2430 break
2431 else:
2432 DieWithError('Couldn\'t find patchset %i in issue %i' %
2433 (parsed_issue_arg.patchset, self.GetIssue()))
2434
2435 fetch_info = revision_info['fetch']['http']
2436 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2437 RunGit(['cherry-pick', 'FETCH_HEAD'])
2438 self.SetIssue(self.GetIssue())
2439 self.SetPatchset(patchset)
2440 print('Committed patch for issue %i pathset %i locally' %
2441 (self.GetIssue(), self.GetPatchset()))
2442 return 0
2443
2444 @staticmethod
2445 def ParseIssueURL(parsed_url):
2446 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2447 return None
2448 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2449 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2450 # Short urls like https://domain/<issue_number> can be used, but don't allow
2451 # specifying the patchset (you'd 404), but we allow that here.
2452 if parsed_url.path == '/':
2453 part = parsed_url.fragment
2454 else:
2455 part = parsed_url.path
2456 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2457 if match:
2458 return _ParsedIssueNumberArgument(
2459 issue=int(match.group(2)),
2460 patchset=int(match.group(4)) if match.group(4) else None,
2461 hostname=parsed_url.netloc)
2462 return None
2463
tandrii16e0b4e2016-06-07 10:34:28 -07002464 def _GerritCommitMsgHookCheck(self, offer_removal):
2465 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2466 if not os.path.exists(hook):
2467 return
2468 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2469 # custom developer made one.
2470 data = gclient_utils.FileRead(hook)
2471 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2472 return
2473 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002474 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002475 'and may interfere with it in subtle ways.\n'
2476 'We recommend you remove the commit-msg hook.')
2477 if offer_removal:
2478 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2479 if reply.lower().startswith('y'):
2480 gclient_utils.rm_file_or_tree(hook)
2481 print('Gerrit commit-msg hook removed.')
2482 else:
2483 print('OK, will keep Gerrit commit-msg hook in place.')
2484
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002485 def CMDUploadChange(self, options, args, change):
2486 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002487 if options.squash and options.no_squash:
2488 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002489
2490 if not options.squash and not options.no_squash:
2491 # Load default for user, repo, squash=true, in this order.
2492 options.squash = settings.GetSquashGerritUploads()
2493 elif options.no_squash:
2494 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002495
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002496 # We assume the remote called "origin" is the one we want.
2497 # It is probably not worthwhile to support different workflows.
2498 gerrit_remote = 'origin'
2499
2500 remote, remote_branch = self.GetRemoteBranch()
2501 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2502 pending_prefix='')
2503
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002504 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002505 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002506 if self.GetIssue():
2507 # Try to get the message from a previous upload.
2508 message = self.GetDescription()
2509 if not message:
2510 DieWithError(
2511 'failed to fetch description from current Gerrit issue %d\n'
2512 '%s' % (self.GetIssue(), self.GetIssueURL()))
2513 change_id = self._GetChangeDetail()['change_id']
2514 while True:
2515 footer_change_ids = git_footers.get_footer_change_id(message)
2516 if footer_change_ids == [change_id]:
2517 break
2518 if not footer_change_ids:
2519 message = git_footers.add_footer_change_id(message, change_id)
2520 print('WARNING: appended missing Change-Id to issue description')
2521 continue
2522 # There is already a valid footer but with different or several ids.
2523 # Doing this automatically is non-trivial as we don't want to lose
2524 # existing other footers, yet we want to append just 1 desired
2525 # Change-Id. Thus, just create a new footer, but let user verify the
2526 # new description.
2527 message = '%s\n\nChange-Id: %s' % (message, change_id)
2528 print(
2529 'WARNING: issue %s has Change-Id footer(s):\n'
2530 ' %s\n'
2531 'but issue has Change-Id %s, according to Gerrit.\n'
2532 'Please, check the proposed correction to the description, '
2533 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2534 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2535 change_id))
2536 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2537 if not options.force:
2538 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002539 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002540 message = change_desc.description
2541 if not message:
2542 DieWithError("Description is empty. Aborting...")
2543 # Continue the while loop.
2544 # Sanity check of this code - we should end up with proper message
2545 # footer.
2546 assert [change_id] == git_footers.get_footer_change_id(message)
2547 change_desc = ChangeDescription(message)
2548 else:
2549 change_desc = ChangeDescription(
2550 options.message or CreateDescriptionFromLog(args))
2551 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002552 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002553 if not change_desc.description:
2554 DieWithError("Description is empty. Aborting...")
2555 message = change_desc.description
2556 change_ids = git_footers.get_footer_change_id(message)
2557 if len(change_ids) > 1:
2558 DieWithError('too many Change-Id footers, at most 1 allowed.')
2559 if not change_ids:
2560 # Generate the Change-Id automatically.
2561 message = git_footers.add_footer_change_id(
2562 message, GenerateGerritChangeId(message))
2563 change_desc.set_description(message)
2564 change_ids = git_footers.get_footer_change_id(message)
2565 assert len(change_ids) == 1
2566 change_id = change_ids[0]
2567
2568 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2569 if remote is '.':
2570 # If our upstream branch is local, we base our squashed commit on its
2571 # squashed version.
2572 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2573 # Check the squashed hash of the parent.
2574 parent = RunGit(['config',
2575 'branch.%s.gerritsquashhash' % upstream_branch_name],
2576 error_ok=True).strip()
2577 # Verify that the upstream branch has been uploaded too, otherwise
2578 # Gerrit will create additional CLs when uploading.
2579 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2580 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002581 DieWithError(
2582 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002583 'Note: maybe you\'ve uploaded it with --no-squash. '
2584 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002585 ' git cl upload --squash\n' % upstream_branch_name)
2586 else:
2587 parent = self.GetCommonAncestorWithUpstream()
2588
2589 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2590 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2591 '-m', message]).strip()
2592 else:
2593 change_desc = ChangeDescription(
2594 options.message or CreateDescriptionFromLog(args))
2595 if not change_desc.description:
2596 DieWithError("Description is empty. Aborting...")
2597
2598 if not git_footers.get_footer_change_id(change_desc.description):
2599 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002600 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2601 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002602 ref_to_push = 'HEAD'
2603 parent = '%s/%s' % (gerrit_remote, branch)
2604 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2605
2606 assert change_desc
2607 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2608 ref_to_push)]).splitlines()
2609 if len(commits) > 1:
2610 print('WARNING: This will upload %d commits. Run the following command '
2611 'to see which commits will be uploaded: ' % len(commits))
2612 print('git log %s..%s' % (parent, ref_to_push))
2613 print('You can also use `git squash-branch` to squash these into a '
2614 'single commit.')
2615 ask_for_data('About to upload; enter to confirm.')
2616
2617 if options.reviewers or options.tbr_owners:
2618 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2619 change)
2620
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002621 # Extra options that can be specified at push time. Doc:
2622 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2623 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002624 if change_desc.get_reviewers(tbr_only=True):
2625 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2626 refspec_opts.append('l=Code-Review+1')
2627
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002628 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002629 if not re.match(r'^[\w ]+$', options.title):
2630 options.title = re.sub(r'[^\w ]', '', options.title)
2631 print('WARNING: Patchset title may only contain alphanumeric chars '
2632 'and spaces. Cleaned up title:\n%s' % options.title)
2633 if not options.force:
2634 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002635 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2636 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002637 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2638
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002639 if options.send_mail:
2640 if not change_desc.get_reviewers():
2641 DieWithError('Must specify reviewers to send email.')
2642 refspec_opts.append('notify=ALL')
2643 else:
2644 refspec_opts.append('notify=NONE')
2645
tandrii99a72f22016-08-17 14:33:24 -07002646 reviewers = change_desc.get_reviewers()
2647 if reviewers:
2648 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002649
agablec6787972016-09-09 16:13:34 -07002650 if options.private:
2651 refspec_opts.append('draft')
2652
rmistry9eadede2016-09-19 11:22:43 -07002653 if options.topic:
2654 # Documentation on Gerrit topics is here:
2655 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2656 refspec_opts.append('topic=%s' % options.topic)
2657
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002658 refspec_suffix = ''
2659 if refspec_opts:
2660 refspec_suffix = '%' + ','.join(refspec_opts)
2661 assert ' ' not in refspec_suffix, (
2662 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002663 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002664
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002665 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002666 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002667 print_stdout=True,
2668 # Flush after every line: useful for seeing progress when running as
2669 # recipe.
2670 filter_fn=lambda _: sys.stdout.flush())
2671
2672 if options.squash:
2673 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2674 change_numbers = [m.group(1)
2675 for m in map(regex.match, push_stdout.splitlines())
2676 if m]
2677 if len(change_numbers) != 1:
2678 DieWithError(
2679 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2680 'Change-Id: %s') % (len(change_numbers), change_id))
2681 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002682 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002683
2684 # Add cc's from the CC_LIST and --cc flag (if any).
2685 cc = self.GetCCList().split(',')
2686 if options.cc:
2687 cc.extend(options.cc)
2688 cc = filter(None, [email.strip() for email in cc])
2689 if cc:
2690 gerrit_util.AddReviewers(
2691 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
2692
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002693 return 0
2694
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002695 def _AddChangeIdToCommitMessage(self, options, args):
2696 """Re-commits using the current message, assumes the commit hook is in
2697 place.
2698 """
2699 log_desc = options.message or CreateDescriptionFromLog(args)
2700 git_command = ['commit', '--amend', '-m', log_desc]
2701 RunGit(git_command)
2702 new_log_desc = CreateDescriptionFromLog(args)
2703 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002704 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002705 return new_log_desc
2706 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002707 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002708
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002709 def SetCQState(self, new_state):
2710 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002711 vote_map = {
2712 _CQState.NONE: 0,
2713 _CQState.DRY_RUN: 1,
2714 _CQState.COMMIT : 2,
2715 }
2716 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2717 labels={'Commit-Queue': vote_map[new_state]})
2718
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002719
2720_CODEREVIEW_IMPLEMENTATIONS = {
2721 'rietveld': _RietveldChangelistImpl,
2722 'gerrit': _GerritChangelistImpl,
2723}
2724
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002725
iannuccie53c9352016-08-17 14:40:40 -07002726def _add_codereview_issue_select_options(parser, extra=""):
2727 _add_codereview_select_options(parser)
2728
2729 text = ('Operate on this issue number instead of the current branch\'s '
2730 'implicit issue.')
2731 if extra:
2732 text += ' '+extra
2733 parser.add_option('-i', '--issue', type=int, help=text)
2734
2735
2736def _process_codereview_issue_select_options(parser, options):
2737 _process_codereview_select_options(parser, options)
2738 if options.issue is not None and not options.forced_codereview:
2739 parser.error('--issue must be specified with either --rietveld or --gerrit')
2740
2741
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002742def _add_codereview_select_options(parser):
2743 """Appends --gerrit and --rietveld options to force specific codereview."""
2744 parser.codereview_group = optparse.OptionGroup(
2745 parser, 'EXPERIMENTAL! Codereview override options')
2746 parser.add_option_group(parser.codereview_group)
2747 parser.codereview_group.add_option(
2748 '--gerrit', action='store_true',
2749 help='Force the use of Gerrit for codereview')
2750 parser.codereview_group.add_option(
2751 '--rietveld', action='store_true',
2752 help='Force the use of Rietveld for codereview')
2753
2754
2755def _process_codereview_select_options(parser, options):
2756 if options.gerrit and options.rietveld:
2757 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2758 options.forced_codereview = None
2759 if options.gerrit:
2760 options.forced_codereview = 'gerrit'
2761 elif options.rietveld:
2762 options.forced_codereview = 'rietveld'
2763
2764
tandriif9aefb72016-07-01 09:06:51 -07002765def _get_bug_line_values(default_project, bugs):
2766 """Given default_project and comma separated list of bugs, yields bug line
2767 values.
2768
2769 Each bug can be either:
2770 * a number, which is combined with default_project
2771 * string, which is left as is.
2772
2773 This function may produce more than one line, because bugdroid expects one
2774 project per line.
2775
2776 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2777 ['v8:123', 'chromium:789']
2778 """
2779 default_bugs = []
2780 others = []
2781 for bug in bugs.split(','):
2782 bug = bug.strip()
2783 if bug:
2784 try:
2785 default_bugs.append(int(bug))
2786 except ValueError:
2787 others.append(bug)
2788
2789 if default_bugs:
2790 default_bugs = ','.join(map(str, default_bugs))
2791 if default_project:
2792 yield '%s:%s' % (default_project, default_bugs)
2793 else:
2794 yield default_bugs
2795 for other in sorted(others):
2796 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2797 yield other
2798
2799
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002800class ChangeDescription(object):
2801 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002802 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002803 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002804
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002805 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002806 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002807
agable@chromium.org42c20792013-09-12 17:34:49 +00002808 @property # www.logilab.org/ticket/89786
2809 def description(self): # pylint: disable=E0202
2810 return '\n'.join(self._description_lines)
2811
2812 def set_description(self, desc):
2813 if isinstance(desc, basestring):
2814 lines = desc.splitlines()
2815 else:
2816 lines = [line.rstrip() for line in desc]
2817 while lines and not lines[0]:
2818 lines.pop(0)
2819 while lines and not lines[-1]:
2820 lines.pop(-1)
2821 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002822
piman@chromium.org336f9122014-09-04 02:16:55 +00002823 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002824 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002825 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002826 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002827 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002828 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002829
agable@chromium.org42c20792013-09-12 17:34:49 +00002830 # Get the set of R= and TBR= lines and remove them from the desciption.
2831 regexp = re.compile(self.R_LINE)
2832 matches = [regexp.match(line) for line in self._description_lines]
2833 new_desc = [l for i, l in enumerate(self._description_lines)
2834 if not matches[i]]
2835 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002836
agable@chromium.org42c20792013-09-12 17:34:49 +00002837 # Construct new unified R= and TBR= lines.
2838 r_names = []
2839 tbr_names = []
2840 for match in matches:
2841 if not match:
2842 continue
2843 people = cleanup_list([match.group(2).strip()])
2844 if match.group(1) == 'TBR':
2845 tbr_names.extend(people)
2846 else:
2847 r_names.extend(people)
2848 for name in r_names:
2849 if name not in reviewers:
2850 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002851 if add_owners_tbr:
2852 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002853 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002854 all_reviewers = set(tbr_names + reviewers)
2855 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2856 all_reviewers)
2857 tbr_names.extend(owners_db.reviewers_for(missing_files,
2858 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002859 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2860 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2861
2862 # Put the new lines in the description where the old first R= line was.
2863 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2864 if 0 <= line_loc < len(self._description_lines):
2865 if new_tbr_line:
2866 self._description_lines.insert(line_loc, new_tbr_line)
2867 if new_r_line:
2868 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002869 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002870 if new_r_line:
2871 self.append_footer(new_r_line)
2872 if new_tbr_line:
2873 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002874
tandriif9aefb72016-07-01 09:06:51 -07002875 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002876 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002877 self.set_description([
2878 '# Enter a description of the change.',
2879 '# This will be displayed on the codereview site.',
2880 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002881 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002882 '--------------------',
2883 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002884
agable@chromium.org42c20792013-09-12 17:34:49 +00002885 regexp = re.compile(self.BUG_LINE)
2886 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002887 prefix = settings.GetBugPrefix()
2888 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2889 for value in values:
2890 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2891 self.append_footer('BUG=%s' % value)
2892
agable@chromium.org42c20792013-09-12 17:34:49 +00002893 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002894 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002895 if not content:
2896 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002897 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002898
2899 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002900 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2901 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002902 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002903 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002904
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002905 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002906 """Adds a footer line to the description.
2907
2908 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2909 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2910 that Gerrit footers are always at the end.
2911 """
2912 parsed_footer_line = git_footers.parse_footer(line)
2913 if parsed_footer_line:
2914 # Line is a gerrit footer in the form: Footer-Key: any value.
2915 # Thus, must be appended observing Gerrit footer rules.
2916 self.set_description(
2917 git_footers.add_footer(self.description,
2918 key=parsed_footer_line[0],
2919 value=parsed_footer_line[1]))
2920 return
2921
2922 if not self._description_lines:
2923 self._description_lines.append(line)
2924 return
2925
2926 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2927 if gerrit_footers:
2928 # git_footers.split_footers ensures that there is an empty line before
2929 # actual (gerrit) footers, if any. We have to keep it that way.
2930 assert top_lines and top_lines[-1] == ''
2931 top_lines, separator = top_lines[:-1], top_lines[-1:]
2932 else:
2933 separator = [] # No need for separator if there are no gerrit_footers.
2934
2935 prev_line = top_lines[-1] if top_lines else ''
2936 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2937 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2938 top_lines.append('')
2939 top_lines.append(line)
2940 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002941
tandrii99a72f22016-08-17 14:33:24 -07002942 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002943 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002944 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002945 reviewers = [match.group(2).strip()
2946 for match in matches
2947 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002948 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002949
2950
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002951def get_approving_reviewers(props):
2952 """Retrieves the reviewers that approved a CL from the issue properties with
2953 messages.
2954
2955 Note that the list may contain reviewers that are not committer, thus are not
2956 considered by the CQ.
2957 """
2958 return sorted(
2959 set(
2960 message['sender']
2961 for message in props['messages']
2962 if message['approval'] and message['sender'] in props['reviewers']
2963 )
2964 )
2965
2966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002967def FindCodereviewSettingsFile(filename='codereview.settings'):
2968 """Finds the given file starting in the cwd and going up.
2969
2970 Only looks up to the top of the repository unless an
2971 'inherit-review-settings-ok' file exists in the root of the repository.
2972 """
2973 inherit_ok_file = 'inherit-review-settings-ok'
2974 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002975 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002976 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2977 root = '/'
2978 while True:
2979 if filename in os.listdir(cwd):
2980 if os.path.isfile(os.path.join(cwd, filename)):
2981 return open(os.path.join(cwd, filename))
2982 if cwd == root:
2983 break
2984 cwd = os.path.dirname(cwd)
2985
2986
2987def LoadCodereviewSettingsFromFile(fileobj):
2988 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002989 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991 def SetProperty(name, setting, unset_error_ok=False):
2992 fullname = 'rietveld.' + name
2993 if setting in keyvals:
2994 RunGit(['config', fullname, keyvals[setting]])
2995 else:
2996 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2997
2998 SetProperty('server', 'CODE_REVIEW_SERVER')
2999 # Only server setting is required. Other settings can be absent.
3000 # In that case, we ignore errors raised during option deletion attempt.
3001 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003002 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003003 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3004 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003005 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003006 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003007 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3008 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003009 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003010 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003011 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
tandriif46c20f2016-09-14 06:17:05 -07003012 SetProperty('git-number-footer', 'GIT_NUMBER_FOOTER', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003013 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3014 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003015
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003016 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003017 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003018
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003019 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003020 RunGit(['config', 'gerrit.squash-uploads',
3021 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003022
tandrii@chromium.org28253532016-04-14 13:46:56 +00003023 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003024 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003025 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3026
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003027 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3028 #should be of the form
3029 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3030 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3031 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3032 keyvals['ORIGIN_URL_CONFIG']])
3033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003034
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003035def urlretrieve(source, destination):
3036 """urllib is broken for SSL connections via a proxy therefore we
3037 can't use urllib.urlretrieve()."""
3038 with open(destination, 'w') as f:
3039 f.write(urllib2.urlopen(source).read())
3040
3041
ukai@chromium.org712d6102013-11-27 00:52:58 +00003042def hasSheBang(fname):
3043 """Checks fname is a #! script."""
3044 with open(fname) as f:
3045 return f.read(2).startswith('#!')
3046
3047
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003048# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3049def DownloadHooks(*args, **kwargs):
3050 pass
3051
3052
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003053def DownloadGerritHook(force):
3054 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003055
3056 Args:
3057 force: True to update hooks. False to install hooks if not present.
3058 """
3059 if not settings.GetIsGerrit():
3060 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003061 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003062 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3063 if not os.access(dst, os.X_OK):
3064 if os.path.exists(dst):
3065 if not force:
3066 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003067 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003068 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003069 if not hasSheBang(dst):
3070 DieWithError('Not a script: %s\n'
3071 'You need to download from\n%s\n'
3072 'into .git/hooks/commit-msg and '
3073 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003074 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3075 except Exception:
3076 if os.path.exists(dst):
3077 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003078 DieWithError('\nFailed to download hooks.\n'
3079 'You need to download from\n%s\n'
3080 'into .git/hooks/commit-msg and '
3081 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003082
3083
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003084
3085def GetRietveldCodereviewSettingsInteractively():
3086 """Prompt the user for settings."""
3087 server = settings.GetDefaultServerUrl(error_ok=True)
3088 prompt = 'Rietveld server (host[:port])'
3089 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3090 newserver = ask_for_data(prompt + ':')
3091 if not server and not newserver:
3092 newserver = DEFAULT_SERVER
3093 if newserver:
3094 newserver = gclient_utils.UpgradeToHttps(newserver)
3095 if newserver != server:
3096 RunGit(['config', 'rietveld.server', newserver])
3097
3098 def SetProperty(initial, caption, name, is_url):
3099 prompt = caption
3100 if initial:
3101 prompt += ' ("x" to clear) [%s]' % initial
3102 new_val = ask_for_data(prompt + ':')
3103 if new_val == 'x':
3104 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3105 elif new_val:
3106 if is_url:
3107 new_val = gclient_utils.UpgradeToHttps(new_val)
3108 if new_val != initial:
3109 RunGit(['config', 'rietveld.' + name, new_val])
3110
3111 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3112 SetProperty(settings.GetDefaultPrivateFlag(),
3113 'Private flag (rietveld only)', 'private', False)
3114 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3115 'tree-status-url', False)
3116 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3117 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3118 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3119 'run-post-upload-hook', False)
3120
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003121@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003123 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003124
tandrii5d0a0422016-09-14 06:24:35 -07003125 print('WARNING: git cl config works for Rietveld only')
3126 # TODO(tandrii): remove this once we switch to Gerrit.
3127 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003128 parser.add_option('--activate-update', action='store_true',
3129 help='activate auto-updating [rietveld] section in '
3130 '.git/config')
3131 parser.add_option('--deactivate-update', action='store_true',
3132 help='deactivate auto-updating [rietveld] section in '
3133 '.git/config')
3134 options, args = parser.parse_args(args)
3135
3136 if options.deactivate_update:
3137 RunGit(['config', 'rietveld.autoupdate', 'false'])
3138 return
3139
3140 if options.activate_update:
3141 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3142 return
3143
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003144 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003145 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003146 return 0
3147
3148 url = args[0]
3149 if not url.endswith('codereview.settings'):
3150 url = os.path.join(url, 'codereview.settings')
3151
3152 # Load code review settings and download hooks (if available).
3153 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3154 return 0
3155
3156
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003157def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003158 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003159 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3160 branch = ShortBranchName(branchref)
3161 _, args = parser.parse_args(args)
3162 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003163 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003164 return RunGit(['config', 'branch.%s.base-url' % branch],
3165 error_ok=False).strip()
3166 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003167 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003168 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3169 error_ok=False).strip()
3170
3171
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003172def color_for_status(status):
3173 """Maps a Changelist status to color, for CMDstatus and other tools."""
3174 return {
3175 'unsent': Fore.RED,
3176 'waiting': Fore.BLUE,
3177 'reply': Fore.YELLOW,
3178 'lgtm': Fore.GREEN,
3179 'commit': Fore.MAGENTA,
3180 'closed': Fore.CYAN,
3181 'error': Fore.WHITE,
3182 }.get(status, Fore.WHITE)
3183
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003184
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003185def get_cl_statuses(changes, fine_grained, max_processes=None):
3186 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003187
3188 If fine_grained is true, this will fetch CL statuses from the server.
3189 Otherwise, simply indicate if there's a matching url for the given branches.
3190
3191 If max_processes is specified, it is used as the maximum number of processes
3192 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3193 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003194
3195 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003196 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003197 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003198 upload.verbosity = 0
3199
3200 if fine_grained:
3201 # Process one branch synchronously to work through authentication, then
3202 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003203 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003204 def fetch(cl):
3205 try:
3206 return (cl, cl.GetStatus())
3207 except:
3208 # See http://crbug.com/629863.
3209 logging.exception('failed to fetch status for %s:', cl)
3210 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003211 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003212
tandriiea9514a2016-08-17 12:32:37 -07003213 changes_to_fetch = changes[1:]
3214 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003215 # Exit early if there was only one branch to fetch.
3216 return
3217
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003218 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003219 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003220 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003221 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003222
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003223 fetched_cls = set()
3224 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003225 while True:
3226 try:
3227 row = it.next(timeout=5)
3228 except multiprocessing.TimeoutError:
3229 break
3230
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003231 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003232 yield row
3233
3234 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003235 for cl in set(changes_to_fetch) - fetched_cls:
3236 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003237
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003238 else:
3239 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003240 for cl in changes:
3241 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003242
rmistry@google.com2dd99862015-06-22 12:22:18 +00003243
3244def upload_branch_deps(cl, args):
3245 """Uploads CLs of local branches that are dependents of the current branch.
3246
3247 If the local branch dependency tree looks like:
3248 test1 -> test2.1 -> test3.1
3249 -> test3.2
3250 -> test2.2 -> test3.3
3251
3252 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3253 run on the dependent branches in this order:
3254 test2.1, test3.1, test3.2, test2.2, test3.3
3255
3256 Note: This function does not rebase your local dependent branches. Use it when
3257 you make a change to the parent branch that will not conflict with its
3258 dependent branches, and you would like their dependencies updated in
3259 Rietveld.
3260 """
3261 if git_common.is_dirty_git_tree('upload-branch-deps'):
3262 return 1
3263
3264 root_branch = cl.GetBranch()
3265 if root_branch is None:
3266 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3267 'Get on a branch!')
3268 if not cl.GetIssue() or not cl.GetPatchset():
3269 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3270 'patchset dependencies without an uploaded CL.')
3271
3272 branches = RunGit(['for-each-ref',
3273 '--format=%(refname:short) %(upstream:short)',
3274 'refs/heads'])
3275 if not branches:
3276 print('No local branches found.')
3277 return 0
3278
3279 # Create a dictionary of all local branches to the branches that are dependent
3280 # on it.
3281 tracked_to_dependents = collections.defaultdict(list)
3282 for b in branches.splitlines():
3283 tokens = b.split()
3284 if len(tokens) == 2:
3285 branch_name, tracked = tokens
3286 tracked_to_dependents[tracked].append(branch_name)
3287
vapiera7fbd5a2016-06-16 09:17:49 -07003288 print()
3289 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003290 dependents = []
3291 def traverse_dependents_preorder(branch, padding=''):
3292 dependents_to_process = tracked_to_dependents.get(branch, [])
3293 padding += ' '
3294 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003295 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003296 dependents.append(dependent)
3297 traverse_dependents_preorder(dependent, padding)
3298 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003299 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003300
3301 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003302 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003303 return 0
3304
vapiera7fbd5a2016-06-16 09:17:49 -07003305 print('This command will checkout all dependent branches and run '
3306 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003307 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3308
andybons@chromium.org962f9462016-02-03 20:00:42 +00003309 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003310 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003311 args.extend(['-t', 'Updated patchset dependency'])
3312
rmistry@google.com2dd99862015-06-22 12:22:18 +00003313 # Record all dependents that failed to upload.
3314 failures = {}
3315 # Go through all dependents, checkout the branch and upload.
3316 try:
3317 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003318 print()
3319 print('--------------------------------------')
3320 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003321 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003322 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003323 try:
3324 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003325 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003326 failures[dependent_branch] = 1
3327 except: # pylint: disable=W0702
3328 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003329 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003330 finally:
3331 # Swap back to the original root branch.
3332 RunGit(['checkout', '-q', root_branch])
3333
vapiera7fbd5a2016-06-16 09:17:49 -07003334 print()
3335 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003336 for dependent_branch in dependents:
3337 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003338 print(' %s : %s' % (dependent_branch, upload_status))
3339 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003340
3341 return 0
3342
3343
kmarshall3bff56b2016-06-06 18:31:47 -07003344def CMDarchive(parser, args):
3345 """Archives and deletes branches associated with closed changelists."""
3346 parser.add_option(
3347 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003348 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003349 parser.add_option(
3350 '-f', '--force', action='store_true',
3351 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003352 parser.add_option(
3353 '-d', '--dry-run', action='store_true',
3354 help='Skip the branch tagging and removal steps.')
3355 parser.add_option(
3356 '-t', '--notags', action='store_true',
3357 help='Do not tag archived branches. '
3358 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003359
3360 auth.add_auth_options(parser)
3361 options, args = parser.parse_args(args)
3362 if args:
3363 parser.error('Unsupported args: %s' % ' '.join(args))
3364 auth_config = auth.extract_auth_config_from_options(options)
3365
3366 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3367 if not branches:
3368 return 0
3369
vapiera7fbd5a2016-06-16 09:17:49 -07003370 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003371 changes = [Changelist(branchref=b, auth_config=auth_config)
3372 for b in branches.splitlines()]
3373 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3374 statuses = get_cl_statuses(changes,
3375 fine_grained=True,
3376 max_processes=options.maxjobs)
3377 proposal = [(cl.GetBranch(),
3378 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3379 for cl, status in statuses
3380 if status == 'closed']
3381 proposal.sort()
3382
3383 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003384 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003385 return 0
3386
3387 current_branch = GetCurrentBranch()
3388
vapiera7fbd5a2016-06-16 09:17:49 -07003389 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003390 if options.notags:
3391 for next_item in proposal:
3392 print(' ' + next_item[0])
3393 else:
3394 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3395 for next_item in proposal:
3396 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003397
kmarshall9249e012016-08-23 12:02:16 -07003398 # Quit now on precondition failure or if instructed by the user, either
3399 # via an interactive prompt or by command line flags.
3400 if options.dry_run:
3401 print('\nNo changes were made (dry run).\n')
3402 return 0
3403 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003404 print('You are currently on a branch \'%s\' which is associated with a '
3405 'closed codereview issue, so archive cannot proceed. Please '
3406 'checkout another branch and run this command again.' %
3407 current_branch)
3408 return 1
kmarshall9249e012016-08-23 12:02:16 -07003409 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003410 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3411 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003412 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003413 return 1
3414
3415 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003416 if not options.notags:
3417 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003418 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003419
vapiera7fbd5a2016-06-16 09:17:49 -07003420 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003421
3422 return 0
3423
3424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003425def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003426 """Show status of changelists.
3427
3428 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003429 - Red not sent for review or broken
3430 - Blue waiting for review
3431 - Yellow waiting for you to reply to review
3432 - Green LGTM'ed
3433 - Magenta in the commit queue
3434 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003435
3436 Also see 'git cl comments'.
3437 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003438 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003439 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003440 parser.add_option('-f', '--fast', action='store_true',
3441 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003442 parser.add_option(
3443 '-j', '--maxjobs', action='store', type=int,
3444 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003445
3446 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003447 _add_codereview_issue_select_options(
3448 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003449 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003450 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003451 if args:
3452 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003453 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003454
iannuccie53c9352016-08-17 14:40:40 -07003455 if options.issue is not None and not options.field:
3456 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003458 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003459 cl = Changelist(auth_config=auth_config, issue=options.issue,
3460 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003461 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003462 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003463 elif options.field == 'id':
3464 issueid = cl.GetIssue()
3465 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003466 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003467 elif options.field == 'patch':
3468 patchset = cl.GetPatchset()
3469 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003470 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003471 elif options.field == 'status':
3472 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003473 elif options.field == 'url':
3474 url = cl.GetIssueURL()
3475 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003476 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003477 return 0
3478
3479 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3480 if not branches:
3481 print('No local branch found.')
3482 return 0
3483
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003484 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003485 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003486 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003487 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003488 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003489 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003490 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003491
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003492 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003493 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3494 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3495 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003496 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003497 c, status = output.next()
3498 branch_statuses[c.GetBranch()] = status
3499 status = branch_statuses.pop(branch)
3500 url = cl.GetIssueURL()
3501 if url and (not status or status == 'error'):
3502 # The issue probably doesn't exist anymore.
3503 url += ' (broken)'
3504
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003505 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003506 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003507 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003508 color = ''
3509 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003510 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003511 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003512 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003513 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003514
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003515 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003516 print()
3517 print('Current branch:',)
3518 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003519 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003520 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003521 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003522 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003523 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003524 print('Issue description:')
3525 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003526 return 0
3527
3528
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003529def colorize_CMDstatus_doc():
3530 """To be called once in main() to add colors to git cl status help."""
3531 colors = [i for i in dir(Fore) if i[0].isupper()]
3532
3533 def colorize_line(line):
3534 for color in colors:
3535 if color in line.upper():
3536 # Extract whitespaces first and the leading '-'.
3537 indent = len(line) - len(line.lstrip(' ')) + 1
3538 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3539 return line
3540
3541 lines = CMDstatus.__doc__.splitlines()
3542 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3543
3544
phajdan.jre328cf92016-08-22 04:12:17 -07003545def write_json(path, contents):
3546 with open(path, 'w') as f:
3547 json.dump(contents, f)
3548
3549
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003550@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003551def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003552 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003553
3554 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003555 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003556 parser.add_option('-r', '--reverse', action='store_true',
3557 help='Lookup the branch(es) for the specified issues. If '
3558 'no issues are specified, all branches with mapped '
3559 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003560 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003561 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003562 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003563 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564
dnj@chromium.org406c4402015-03-03 17:22:28 +00003565 if options.reverse:
3566 branches = RunGit(['for-each-ref', 'refs/heads',
3567 '--format=%(refname:short)']).splitlines()
3568
3569 # Reverse issue lookup.
3570 issue_branch_map = {}
3571 for branch in branches:
3572 cl = Changelist(branchref=branch)
3573 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3574 if not args:
3575 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003576 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003577 for issue in args:
3578 if not issue:
3579 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003580 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003581 print('Branch for issue number %s: %s' % (
3582 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003583 if options.json:
3584 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003585 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003586 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003587 if len(args) > 0:
3588 try:
3589 issue = int(args[0])
3590 except ValueError:
3591 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003592 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003593 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003595 if options.json:
3596 write_json(options.json, {
3597 'issue': cl.GetIssue(),
3598 'issue_url': cl.GetIssueURL(),
3599 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003600 return 0
3601
3602
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003603def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003604 """Shows or posts review comments for any changelist."""
3605 parser.add_option('-a', '--add-comment', dest='comment',
3606 help='comment to add to an issue')
3607 parser.add_option('-i', dest='issue',
3608 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003609 parser.add_option('-j', '--json-file',
3610 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003612 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003613 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003614
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003615 issue = None
3616 if options.issue:
3617 try:
3618 issue = int(options.issue)
3619 except ValueError:
3620 DieWithError('A review issue id is expected to be a number')
3621
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003622 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003623
3624 if options.comment:
3625 cl.AddComment(options.comment)
3626 return 0
3627
3628 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003629 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003630 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003631 summary.append({
3632 'date': message['date'],
3633 'lgtm': False,
3634 'message': message['text'],
3635 'not_lgtm': False,
3636 'sender': message['sender'],
3637 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003638 if message['disapproval']:
3639 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003640 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003641 elif message['approval']:
3642 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003643 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003644 elif message['sender'] == data['owner_email']:
3645 color = Fore.MAGENTA
3646 else:
3647 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003648 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003649 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003650 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003651 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003652 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003653 if options.json_file:
3654 with open(options.json_file, 'wb') as f:
3655 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003656 return 0
3657
3658
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003659@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003660def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003661 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003662 parser.add_option('-d', '--display', action='store_true',
3663 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003664 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003665 help='New description to set for this issue (- for stdin, '
3666 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003667 parser.add_option('-f', '--force', action='store_true',
3668 help='Delete any unpublished Gerrit edits for this issue '
3669 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003670
3671 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003672 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003673 options, args = parser.parse_args(args)
3674 _process_codereview_select_options(parser, options)
3675
3676 target_issue = None
3677 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003678 target_issue = ParseIssueNumberArgument(args[0])
3679 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003680 parser.print_help()
3681 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003682
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003683 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003684
martiniss6eda05f2016-06-30 10:18:35 -07003685 kwargs = {
3686 'auth_config': auth_config,
3687 'codereview': options.forced_codereview,
3688 }
3689 if target_issue:
3690 kwargs['issue'] = target_issue.issue
3691 if options.forced_codereview == 'rietveld':
3692 kwargs['rietveld_server'] = target_issue.hostname
3693
3694 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003695
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003696 if not cl.GetIssue():
3697 DieWithError('This branch has no associated changelist.')
3698 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003699
smut@google.com34fb6b12015-07-13 20:03:26 +00003700 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003701 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003702 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003703
3704 if options.new_description:
3705 text = options.new_description
3706 if text == '-':
3707 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003708 elif text == '+':
3709 base_branch = cl.GetCommonAncestorWithUpstream()
3710 change = cl.GetChange(base_branch, None, local_description=True)
3711 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003712
3713 description.set_description(text)
3714 else:
3715 description.prompt()
3716
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003717 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003718 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003719 return 0
3720
3721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003722def CreateDescriptionFromLog(args):
3723 """Pulls out the commit log to use as a base for the CL description."""
3724 log_args = []
3725 if len(args) == 1 and not args[0].endswith('.'):
3726 log_args = [args[0] + '..']
3727 elif len(args) == 1 and args[0].endswith('...'):
3728 log_args = [args[0][:-1]]
3729 elif len(args) == 2:
3730 log_args = [args[0] + '..' + args[1]]
3731 else:
3732 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003733 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003734
3735
thestig@chromium.org44202a22014-03-11 19:22:18 +00003736def CMDlint(parser, args):
3737 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003738 parser.add_option('--filter', action='append', metavar='-x,+y',
3739 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003740 auth.add_auth_options(parser)
3741 options, args = parser.parse_args(args)
3742 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003743
3744 # Access to a protected member _XX of a client class
3745 # pylint: disable=W0212
3746 try:
3747 import cpplint
3748 import cpplint_chromium
3749 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003750 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003751 return 1
3752
3753 # Change the current working directory before calling lint so that it
3754 # shows the correct base.
3755 previous_cwd = os.getcwd()
3756 os.chdir(settings.GetRoot())
3757 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003758 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003759 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3760 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003761 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003762 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003763 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003764
3765 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003766 command = args + files
3767 if options.filter:
3768 command = ['--filter=' + ','.join(options.filter)] + command
3769 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003770
3771 white_regex = re.compile(settings.GetLintRegex())
3772 black_regex = re.compile(settings.GetLintIgnoreRegex())
3773 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3774 for filename in filenames:
3775 if white_regex.match(filename):
3776 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003777 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003778 else:
3779 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3780 extra_check_functions)
3781 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003782 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003783 finally:
3784 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003786 if cpplint._cpplint_state.error_count != 0:
3787 return 1
3788 return 0
3789
3790
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003792 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003793 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003794 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003795 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003796 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003797 auth.add_auth_options(parser)
3798 options, args = parser.parse_args(args)
3799 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003800
sbc@chromium.org71437c02015-04-09 19:29:40 +00003801 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003802 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003803 return 1
3804
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003805 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806 if args:
3807 base_branch = args[0]
3808 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003809 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003810 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003812 cl.RunHook(
3813 committing=not options.upload,
3814 may_prompt=False,
3815 verbose=options.verbose,
3816 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003817 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003818
3819
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003820def GenerateGerritChangeId(message):
3821 """Returns Ixxxxxx...xxx change id.
3822
3823 Works the same way as
3824 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3825 but can be called on demand on all platforms.
3826
3827 The basic idea is to generate git hash of a state of the tree, original commit
3828 message, author/committer info and timestamps.
3829 """
3830 lines = []
3831 tree_hash = RunGitSilent(['write-tree'])
3832 lines.append('tree %s' % tree_hash.strip())
3833 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3834 if code == 0:
3835 lines.append('parent %s' % parent.strip())
3836 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3837 lines.append('author %s' % author.strip())
3838 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3839 lines.append('committer %s' % committer.strip())
3840 lines.append('')
3841 # Note: Gerrit's commit-hook actually cleans message of some lines and
3842 # whitespace. This code is not doing this, but it clearly won't decrease
3843 # entropy.
3844 lines.append(message)
3845 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3846 stdin='\n'.join(lines))
3847 return 'I%s' % change_hash.strip()
3848
3849
wittman@chromium.org455dc922015-01-26 20:15:50 +00003850def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3851 """Computes the remote branch ref to use for the CL.
3852
3853 Args:
3854 remote (str): The git remote for the CL.
3855 remote_branch (str): The git remote branch for the CL.
3856 target_branch (str): The target branch specified by the user.
3857 pending_prefix (str): The pending prefix from the settings.
3858 """
3859 if not (remote and remote_branch):
3860 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003861
wittman@chromium.org455dc922015-01-26 20:15:50 +00003862 if target_branch:
3863 # Cannonicalize branch references to the equivalent local full symbolic
3864 # refs, which are then translated into the remote full symbolic refs
3865 # below.
3866 if '/' not in target_branch:
3867 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3868 else:
3869 prefix_replacements = (
3870 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3871 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3872 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3873 )
3874 match = None
3875 for regex, replacement in prefix_replacements:
3876 match = re.search(regex, target_branch)
3877 if match:
3878 remote_branch = target_branch.replace(match.group(0), replacement)
3879 break
3880 if not match:
3881 # This is a branch path but not one we recognize; use as-is.
3882 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003883 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3884 # Handle the refs that need to land in different refs.
3885 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003886
wittman@chromium.org455dc922015-01-26 20:15:50 +00003887 # Create the true path to the remote branch.
3888 # Does the following translation:
3889 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3890 # * refs/remotes/origin/master -> refs/heads/master
3891 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3892 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3893 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3894 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3895 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3896 'refs/heads/')
3897 elif remote_branch.startswith('refs/remotes/branch-heads'):
3898 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3899 # If a pending prefix exists then replace refs/ with it.
3900 if pending_prefix:
3901 remote_branch = remote_branch.replace('refs/', pending_prefix)
3902 return remote_branch
3903
3904
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003905def cleanup_list(l):
3906 """Fixes a list so that comma separated items are put as individual items.
3907
3908 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3909 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3910 """
3911 items = sum((i.split(',') for i in l), [])
3912 stripped_items = (i.strip() for i in items)
3913 return sorted(filter(None, stripped_items))
3914
3915
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003916@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003917def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003918 """Uploads the current changelist to codereview.
3919
3920 Can skip dependency patchset uploads for a branch by running:
3921 git config branch.branch_name.skip-deps-uploads True
3922 To unset run:
3923 git config --unset branch.branch_name.skip-deps-uploads
3924 Can also set the above globally by using the --global flag.
3925 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003926 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3927 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003928 parser.add_option('--bypass-watchlists', action='store_true',
3929 dest='bypass_watchlists',
3930 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003931 parser.add_option('-f', action='store_true', dest='force',
3932 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003933 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003934 parser.add_option('-b', '--bug',
3935 help='pre-populate the bug number(s) for this issue. '
3936 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003937 parser.add_option('--message-file', dest='message_file',
3938 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003939 parser.add_option('-t', dest='title',
3940 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003941 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003942 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003943 help='reviewer email addresses')
3944 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003945 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003946 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003947 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003948 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003949 parser.add_option('--emulate_svn_auto_props',
3950 '--emulate-svn-auto-props',
3951 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003952 dest="emulate_svn_auto_props",
3953 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003954 parser.add_option('-c', '--use-commit-queue', action='store_true',
3955 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003956 parser.add_option('--private', action='store_true',
3957 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003958 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003959 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003960 metavar='TARGET',
3961 help='Apply CL to remote ref TARGET. ' +
3962 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003963 parser.add_option('--squash', action='store_true',
3964 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003965 parser.add_option('--no-squash', action='store_true',
3966 help='Don\'t squash multiple commits into one ' +
3967 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07003968 parser.add_option('--topic', default=None,
3969 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003970 parser.add_option('--email', default=None,
3971 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003972 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3973 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003974 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3975 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003976 help='Send the patchset to do a CQ dry run right after '
3977 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003978 parser.add_option('--dependencies', action='store_true',
3979 help='Uploads CLs of all the local branches that depend on '
3980 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003981
rmistry@google.com2dd99862015-06-22 12:22:18 +00003982 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003983 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003984 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003985 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003986 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003987 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003988 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003989
sbc@chromium.org71437c02015-04-09 19:29:40 +00003990 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003991 return 1
3992
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003993 options.reviewers = cleanup_list(options.reviewers)
3994 options.cc = cleanup_list(options.cc)
3995
tandriib80458a2016-06-23 12:20:07 -07003996 if options.message_file:
3997 if options.message:
3998 parser.error('only one of --message and --message-file allowed.')
3999 options.message = gclient_utils.FileRead(options.message_file)
4000 options.message_file = None
4001
tandrii4d0545a2016-07-06 03:56:49 -07004002 if options.cq_dry_run and options.use_commit_queue:
4003 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4004
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004005 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4006 settings.GetIsGerrit()
4007
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004008 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004009 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004010
4011
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004012def IsSubmoduleMergeCommit(ref):
4013 # When submodules are added to the repo, we expect there to be a single
4014 # non-git-svn merge commit at remote HEAD with a signature comment.
4015 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004016 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004017 return RunGit(cmd) != ''
4018
4019
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004020def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004021 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004022
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004023 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4024 upstream and closes the issue automatically and atomically.
4025
4026 Otherwise (in case of Rietveld):
4027 Squashes branch into a single commit.
4028 Updates changelog with metadata (e.g. pointer to review).
4029 Pushes/dcommits the code upstream.
4030 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031 """
4032 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4033 help='bypass upload presubmit hook')
4034 parser.add_option('-m', dest='message',
4035 help="override review description")
4036 parser.add_option('-f', action='store_true', dest='force',
4037 help="force yes to questions (don't prompt)")
4038 parser.add_option('-c', dest='contributor',
4039 help="external contributor for patch (appended to " +
4040 "description and used as author for git). Should be " +
4041 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004042 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004043 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004045 auth_config = auth.extract_auth_config_from_options(options)
4046
4047 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004049 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4050 if cl.IsGerrit():
4051 if options.message:
4052 # This could be implemented, but it requires sending a new patch to
4053 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4054 # Besides, Gerrit has the ability to change the commit message on submit
4055 # automatically, thus there is no need to support this option (so far?).
4056 parser.error('-m MESSAGE option is not supported for Gerrit.')
4057 if options.contributor:
4058 parser.error(
4059 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4060 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4061 'the contributor\'s "name <email>". If you can\'t upload such a '
4062 'commit for review, contact your repository admin and request'
4063 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004064 if not cl.GetIssue():
4065 DieWithError('You must upload the issue first to Gerrit.\n'
4066 ' If you would rather have `git cl land` upload '
4067 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004068 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4069 options.verbose)
4070
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004071 current = cl.GetBranch()
4072 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4073 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004074 print()
4075 print('Attempting to push branch %r into another local branch!' % current)
4076 print()
4077 print('Either reparent this branch on top of origin/master:')
4078 print(' git reparent-branch --root')
4079 print()
4080 print('OR run `git rebase-update` if you think the parent branch is ')
4081 print('already committed.')
4082 print()
4083 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004084 return 1
4085
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004086 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087 # Default to merging against our best guess of the upstream branch.
4088 args = [cl.GetUpstreamBranch()]
4089
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004090 if options.contributor:
4091 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004092 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004093 return 1
4094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004096 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004097
sbc@chromium.org71437c02015-04-09 19:29:40 +00004098 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004099 return 1
4100
4101 # This rev-list syntax means "show all commits not in my branch that
4102 # are in base_branch".
4103 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4104 base_branch]).splitlines()
4105 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004106 print('Base branch "%s" has %d commits '
4107 'not in this branch.' % (base_branch, len(upstream_commits)))
4108 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004109 return 1
4110
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004111 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004112 svn_head = None
4113 if cmd == 'dcommit' or base_has_submodules:
4114 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4115 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004117 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004118 # If the base_head is a submodule merge commit, the first parent of the
4119 # base_head should be a git-svn commit, which is what we're interested in.
4120 base_svn_head = base_branch
4121 if base_has_submodules:
4122 base_svn_head += '^1'
4123
4124 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004125 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004126 print('This branch has %d additional commits not upstreamed yet.'
4127 % len(extra_commits.splitlines()))
4128 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4129 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004130 return 1
4131
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004132 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004133 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004134 author = None
4135 if options.contributor:
4136 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004137 hook_results = cl.RunHook(
4138 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004139 may_prompt=not options.force,
4140 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004141 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004142 if not hook_results.should_continue():
4143 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004144
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004145 # Check the tree status if the tree status URL is set.
4146 status = GetTreeStatus()
4147 if 'closed' == status:
4148 print('The tree is closed. Please wait for it to reopen. Use '
4149 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4150 return 1
4151 elif 'unknown' == status:
4152 print('Unable to determine tree status. Please verify manually and '
4153 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4154 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004155
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004156 change_desc = ChangeDescription(options.message)
4157 if not change_desc.description and cl.GetIssue():
4158 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004159
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004160 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004161 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004162 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004163 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004164 print('No description set.')
4165 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004166 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004167
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004168 # Keep a separate copy for the commit message, because the commit message
4169 # contains the link to the Rietveld issue, while the Rietveld message contains
4170 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004171 # Keep a separate copy for the commit message.
4172 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004173 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004174
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004175 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004176 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004177 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004178 # after it. Add a period on a new line to circumvent this. Also add a space
4179 # before the period to make sure that Gitiles continues to correctly resolve
4180 # the URL.
4181 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004182 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004183 commit_desc.append_footer('Patch from %s.' % options.contributor)
4184
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004185 print('Description:')
4186 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004187
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004188 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004189 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004190 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004191
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004192 # We want to squash all this branch's commits into one commit with the proper
4193 # description. We do this by doing a "reset --soft" to the base branch (which
4194 # keeps the working copy the same), then dcommitting that. If origin/master
4195 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4196 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004197 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004198 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4199 # Delete the branches if they exist.
4200 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4201 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4202 result = RunGitWithCode(showref_cmd)
4203 if result[0] == 0:
4204 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004205
4206 # We might be in a directory that's present in this branch but not in the
4207 # trunk. Move up to the top of the tree so that git commands that expect a
4208 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004209 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004210 if rel_base_path:
4211 os.chdir(rel_base_path)
4212
4213 # Stuff our change into the merge branch.
4214 # We wrap in a try...finally block so if anything goes wrong,
4215 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004216 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004217 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004218 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004219 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004220 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004221 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004222 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004224 RunGit(
4225 [
4226 'commit', '--author', options.contributor,
4227 '-m', commit_desc.description,
4228 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004230 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004231 if base_has_submodules:
4232 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4233 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4234 RunGit(['checkout', CHERRY_PICK_BRANCH])
4235 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004236 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004237 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004238 mirror = settings.GetGitMirror(remote)
4239 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004240 pending_prefix = settings.GetPendingRefPrefix()
4241 if not pending_prefix or branch.startswith(pending_prefix):
4242 # If not using refs/pending/heads/* at all, or target ref is already set
4243 # to pending, then push to the target ref directly.
4244 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004245 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004246 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004247 else:
4248 # Cherry-pick the change on top of pending ref and then push it.
4249 assert branch.startswith('refs/'), branch
4250 assert pending_prefix[-1] == '/', pending_prefix
4251 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004252 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004253 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004254 if retcode == 0:
4255 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004256 else:
4257 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004258 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004259 'svn', 'dcommit',
4260 '-C%s' % options.similarity,
4261 '--no-rebase', '--rmdir',
4262 ]
4263 if settings.GetForceHttpsCommitUrl():
4264 # Allow forcing https commit URLs for some projects that don't allow
4265 # committing to http URLs (like Google Code).
4266 remote_url = cl.GetGitSvnRemoteUrl()
4267 if urlparse.urlparse(remote_url).scheme == 'http':
4268 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004269 cmd_args.append('--commit-url=%s' % remote_url)
4270 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004271 if 'Committed r' in output:
4272 revision = re.match(
4273 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4274 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275 finally:
4276 # And then swap back to the original branch and clean up.
4277 RunGit(['checkout', '-q', cl.GetBranch()])
4278 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004279 if base_has_submodules:
4280 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004281
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004282 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004283 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004284 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004285
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004286 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004287 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004288 try:
4289 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4290 # We set pushed_to_pending to False, since it made it all the way to the
4291 # real ref.
4292 pushed_to_pending = False
4293 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004294 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004296 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004297 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004298 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004299 if not to_pending:
4300 if viewvc_url and revision:
4301 change_desc.append_footer(
4302 'Committed: %s%s' % (viewvc_url, revision))
4303 elif revision:
4304 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004305 print('Closing issue '
4306 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004307 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004308 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004309 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004310 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004311 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004312 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004313 if options.bypass_hooks:
4314 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4315 else:
4316 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004317 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004318
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004319 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004320 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004321 print('The commit is in the pending queue (%s).' % pending_ref)
4322 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4323 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004324
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004325 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4326 if os.path.isfile(hook):
4327 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004328
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004329 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004330
4331
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004332def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004333 print()
4334 print('Waiting for commit to be landed on %s...' % real_ref)
4335 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004336 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4337 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004338 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004339
4340 loop = 0
4341 while True:
4342 sys.stdout.write('fetching (%d)... \r' % loop)
4343 sys.stdout.flush()
4344 loop += 1
4345
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004346 if mirror:
4347 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004348 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4349 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4350 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4351 for commit in commits.splitlines():
4352 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004353 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004354 return commit
4355
4356 current_rev = to_rev
4357
4358
tandriibf429402016-09-14 07:09:12 -07004359def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004360 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4361
4362 Returns:
4363 (retcode of last operation, output log of last operation).
4364 """
4365 assert pending_ref.startswith('refs/'), pending_ref
4366 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4367 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4368 code = 0
4369 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004370 max_attempts = 3
4371 attempts_left = max_attempts
4372 while attempts_left:
4373 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004374 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004375 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004376
4377 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004378 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004379 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004380 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004381 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004382 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004383 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004384 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004385 continue
4386
4387 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004388 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004389 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004390 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004391 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004392 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4393 'the following files have merge conflicts:' % pending_ref)
4394 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4395 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004396 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004397 return code, out
4398
4399 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004400 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004401 code, out = RunGitWithCode(
4402 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4403 if code == 0:
4404 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004405 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004406 return code, out
4407
vapiera7fbd5a2016-06-16 09:17:49 -07004408 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004409 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004410 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004411 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004412 print('Fatal push error. Make sure your .netrc credentials and git '
4413 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004414 return code, out
4415
vapiera7fbd5a2016-06-16 09:17:49 -07004416 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004417 return code, out
4418
4419
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004420def IsFatalPushFailure(push_stdout):
4421 """True if retrying push won't help."""
4422 return '(prohibited by Gerrit)' in push_stdout
4423
4424
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004425@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004426def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004427 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004428 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004429 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004430 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004431 message = """This repository appears to be a git-svn mirror, but we
4432don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004433 else:
4434 message = """This doesn't appear to be an SVN repository.
4435If your project has a true, writeable git repository, you probably want to run
4436'git cl land' instead.
4437If your project has a git mirror of an upstream SVN master, you probably need
4438to run 'git svn init'.
4439
4440Using the wrong command might cause your commit to appear to succeed, and the
4441review to be closed, without actually landing upstream. If you choose to
4442proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004443 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004444 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004445 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4446 'Please let us know of this project you are committing to:'
4447 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004448 return SendUpstream(parser, args, 'dcommit')
4449
4450
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004451@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004452def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004453 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004454 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004455 print('This appears to be an SVN repository.')
4456 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004457 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004458 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004459 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004460
4461
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004462@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004463def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004464 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004465 parser.add_option('-b', dest='newbranch',
4466 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004467 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004468 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004469 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4470 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004471 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004472 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004473 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004474 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004475 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004476 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004477
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004478
4479 group = optparse.OptionGroup(
4480 parser,
4481 'Options for continuing work on the current issue uploaded from a '
4482 'different clone (e.g. different machine). Must be used independently '
4483 'from the other options. No issue number should be specified, and the '
4484 'branch must have an issue number associated with it')
4485 group.add_option('--reapply', action='store_true', dest='reapply',
4486 help='Reset the branch and reapply the issue.\n'
4487 'CAUTION: This will undo any local changes in this '
4488 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004489
4490 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004491 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004492 parser.add_option_group(group)
4493
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004494 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004495 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004496 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004497 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004498 auth_config = auth.extract_auth_config_from_options(options)
4499
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004500
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004501 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004502 if options.newbranch:
4503 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004504 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004505 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004506
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004507 cl = Changelist(auth_config=auth_config,
4508 codereview=options.forced_codereview)
4509 if not cl.GetIssue():
4510 parser.error('current branch must have an associated issue')
4511
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004512 upstream = cl.GetUpstreamBranch()
4513 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004514 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004515
4516 RunGit(['reset', '--hard', upstream])
4517 if options.pull:
4518 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004519
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004520 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4521 options.directory)
4522
4523 if len(args) != 1 or not args[0]:
4524 parser.error('Must specify issue number or url')
4525
4526 # We don't want uncommitted changes mixed up with the patch.
4527 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004528 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004530 if options.newbranch:
4531 if options.force:
4532 RunGit(['branch', '-D', options.newbranch],
4533 stderr=subprocess2.PIPE, error_ok=True)
4534 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004535 elif not GetCurrentBranch():
4536 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004537
4538 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4539
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004540 if cl.IsGerrit():
4541 if options.reject:
4542 parser.error('--reject is not supported with Gerrit codereview.')
4543 if options.nocommit:
4544 parser.error('--nocommit is not supported with Gerrit codereview.')
4545 if options.directory:
4546 parser.error('--directory is not supported with Gerrit codereview.')
4547
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004548 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004549 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004550
4551
4552def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004553 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004554 # Provide a wrapper for git svn rebase to help avoid accidental
4555 # git svn dcommit.
4556 # It's the only command that doesn't use parser at all since we just defer
4557 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004558
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004559 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004560
4561
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004562def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004563 """Fetches the tree status and returns either 'open', 'closed',
4564 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004565 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004566 if url:
4567 status = urllib2.urlopen(url).read().lower()
4568 if status.find('closed') != -1 or status == '0':
4569 return 'closed'
4570 elif status.find('open') != -1 or status == '1':
4571 return 'open'
4572 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004573 return 'unset'
4574
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004575
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004576def GetTreeStatusReason():
4577 """Fetches the tree status from a json url and returns the message
4578 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004579 url = settings.GetTreeStatusUrl()
4580 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004581 connection = urllib2.urlopen(json_url)
4582 status = json.loads(connection.read())
4583 connection.close()
4584 return status['message']
4585
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004586
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004587def GetBuilderMaster(bot_list):
4588 """For a given builder, fetch the master from AE if available."""
4589 map_url = 'https://builders-map.appspot.com/'
4590 try:
4591 master_map = json.load(urllib2.urlopen(map_url))
4592 except urllib2.URLError as e:
4593 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4594 (map_url, e))
4595 except ValueError as e:
4596 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4597 if not master_map:
4598 return None, 'Failed to build master map.'
4599
4600 result_master = ''
4601 for bot in bot_list:
4602 builder = bot.split(':', 1)[0]
4603 master_list = master_map.get(builder, [])
4604 if not master_list:
4605 return None, ('No matching master for builder %s.' % builder)
4606 elif len(master_list) > 1:
4607 return None, ('The builder name %s exists in multiple masters %s.' %
4608 (builder, master_list))
4609 else:
4610 cur_master = master_list[0]
4611 if not result_master:
4612 result_master = cur_master
4613 elif result_master != cur_master:
4614 return None, 'The builders do not belong to the same master.'
4615 return result_master, None
4616
4617
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004618def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004619 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004620 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004621 status = GetTreeStatus()
4622 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004623 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004624 return 2
4625
vapiera7fbd5a2016-06-16 09:17:49 -07004626 print('The tree is %s' % status)
4627 print()
4628 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004629 if status != 'open':
4630 return 1
4631 return 0
4632
4633
maruel@chromium.org15192402012-09-06 12:38:29 +00004634def CMDtry(parser, args):
tandriif7b29d42016-10-07 08:45:41 -07004635 """Triggers try jobs using CQ dry run or BuildBucket for individual builders.
4636 """
tandrii1838bad2016-10-06 00:10:52 -07004637 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004638 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004639 '-b', '--bot', action='append',
4640 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4641 'times to specify multiple builders. ex: '
4642 '"-b win_rel -b win_layout". See '
4643 'the try server waterfall for the builders name and the tests '
4644 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004645 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004646 '-m', '--master', default='',
4647 help=('Specify a try master where to run the tries.'))
tandriif7b29d42016-10-07 08:45:41 -07004648 # TODO(tandrii,nodir): add -B --bucket flag.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004649 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004650 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004651 help='Revision to use for the try job; default: the revision will '
4652 'be determined by the try recipe that builder runs, which usually '
4653 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004654 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004655 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004656 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004657 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004658 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004659 '--project',
4660 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004661 'in recipe to determine to which repository or directory to '
4662 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004663 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004664 '-p', '--property', dest='properties', action='append', default=[],
4665 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004666 'key2=value2 etc. The value will be treated as '
4667 'json if decodable, or as string otherwise. '
4668 'NOTE: using this may make your try job not usable for CQ, '
4669 'which will then schedule another try job with default properties')
4670 # TODO(tandrii): if this even used?
machenbach@chromium.org45453142015-09-15 08:45:22 +00004671 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004672 '-n', '--name', help='Try job name; default to current branch name')
tandriif7b29d42016-10-07 08:45:41 -07004673 # TODO(tandrii): get rid of this.
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004674 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004675 '--use-rietveld', action='store_true', default=False,
tandrii8e229542016-10-10 03:23:01 -07004676 help='DEPRECATED, NOT SUPPORTED.')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004677 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004678 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4679 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004680 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004681 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004682 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004683 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004684
tandrii8e229542016-10-10 03:23:01 -07004685 if options.use_rietveld:
4686 parser.error('--use-rietveld is not longer supported.')
machenbach@chromium.org45453142015-09-15 08:45:22 +00004687
4688 # Make sure that all properties are prop=value pairs.
4689 bad_params = [x for x in options.properties if '=' not in x]
4690 if bad_params:
4691 parser.error('Got properties with missing "=": %s' % bad_params)
4692
maruel@chromium.org15192402012-09-06 12:38:29 +00004693 if args:
4694 parser.error('Unknown arguments: %s' % args)
4695
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004696 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004697 if not cl.GetIssue():
4698 parser.error('Need to upload first')
4699
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004700 if cl.IsGerrit():
4701 parser.error(
4702 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4703 'If your project has Commit Queue, dry run is a workaround:\n'
4704 ' git cl set-commit --dry-run')
4705 # Code below assumes Rietveld issue.
4706 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4707
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004708 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004709 if props.get('closed'):
qyearsleyeab3c042016-08-24 09:18:28 -07004710 parser.error('Cannot send try jobs for a closed CL')
agable@chromium.org787e3062014-08-20 16:31:19 +00004711
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004712 if props.get('private'):
qyearsleyeab3c042016-08-24 09:18:28 -07004713 parser.error('Cannot use try bots with private issue')
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004714
maruel@chromium.org15192402012-09-06 12:38:29 +00004715 if not options.name:
4716 options.name = cl.GetBranch()
4717
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004718 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004719 options.master, err_msg = GetBuilderMaster(options.bot)
4720 if err_msg:
4721 parser.error('Tryserver master cannot be found because: %s\n'
4722 'Please manually specify the tryserver master'
4723 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004724
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004725 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004726 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004727 if not options.bot:
4728 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004729
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004730 # Get try masters from PRESUBMIT.py files.
4731 masters = presubmit_support.DoGetTryMasters(
4732 change,
4733 change.LocalPaths(),
4734 settings.GetRoot(),
4735 None,
4736 None,
4737 options.verbose,
4738 sys.stdout)
4739 if masters:
4740 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004741
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004742 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4743 options.bot = presubmit_support.DoGetTrySlaves(
4744 change,
4745 change.LocalPaths(),
4746 settings.GetRoot(),
4747 None,
4748 None,
4749 options.verbose,
4750 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004751
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004752 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004753 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004754
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004755 builders_and_tests = {}
4756 # TODO(machenbach): The old style command-line options don't support
4757 # multiple try masters yet.
4758 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4759 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4760
4761 for bot in old_style:
4762 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004763 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004764 elif ',' in bot:
4765 parser.error('Specify one bot per --bot flag')
4766 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004767 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004768
4769 for bot, tests in new_style:
4770 builders_and_tests.setdefault(bot, []).extend(tests)
4771
4772 # Return a master map with one master to be backwards compatible. The
4773 # master name defaults to an empty string, which will cause the master
4774 # not to be set on rietveld (deprecated).
4775 return {options.master: builders_and_tests}
4776
4777 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004778 if not masters:
4779 # Default to triggering Dry Run (see http://crbug.com/625697).
4780 if options.verbose:
4781 print('git cl try with no bots now defaults to CQ Dry Run.')
4782 try:
4783 cl.SetCQState(_CQState.DRY_RUN)
4784 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4785 return 0
4786 except KeyboardInterrupt:
4787 raise
4788 except:
4789 print('WARNING: failed to trigger CQ Dry Run.\n'
4790 'Either:\n'
4791 ' * your project has no CQ\n'
4792 ' * you don\'t have permission to trigger Dry Run\n'
4793 ' * bug in this code (see stack trace below).\n'
4794 'Consider specifying which bots to trigger manually '
4795 'or asking your project owners for permissions '
4796 'or contacting Chrome Infrastructure team at '
4797 'https://www.chromium.org/infra\n\n')
4798 # Still raise exception so that stack trace is printed.
4799 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004800
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004801 for builders in masters.itervalues():
4802 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004803 print('ERROR You are trying to send a job to a triggered bot. This type '
4804 'of bot requires an\ninitial job from a parent (usually a builder).'
4805 ' Instead send your job to the parent.\n'
4806 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004807 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004808
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004809 patchset = cl.GetMostRecentPatchset()
4810 if patchset and patchset != cl.GetPatchset():
4811 print(
4812 '\nWARNING Mismatch between local config and server. Did a previous '
4813 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4814 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004815 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004816 try:
4817 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4818 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004819 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004820 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004821 except Exception as e:
4822 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
qyearsleyeab3c042016-08-24 09:18:28 -07004823 print('ERROR: Exception when trying to trigger try jobs: %s\n%s' %
vapiera7fbd5a2016-06-16 09:17:49 -07004824 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004825 return 1
4826 else:
4827 try:
4828 cl.RpcServer().trigger_distributed_try_jobs(
4829 cl.GetIssue(), patchset, options.name, options.clobber,
4830 options.revision, masters)
4831 except urllib2.HTTPError as e:
4832 if e.code == 404:
4833 print('404 from rietveld; '
4834 'did you mean to use "git try" instead of "git cl try"?')
4835 return 1
4836 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004837
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004838 for (master, builders) in sorted(masters.iteritems()):
4839 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004840 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004841 length = max(len(builder) for builder in builders)
4842 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004843 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004844 return 0
4845
4846
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004847def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004848 """Prints info about try jobs associated with current CL."""
4849 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004850 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004851 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004852 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004853 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004854 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004855 '--color', action='store_true', default=setup_color.IS_TTY,
4856 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004857 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004858 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4859 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004860 group.add_option(
4861 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004862 parser.add_option_group(group)
4863 auth.add_auth_options(parser)
4864 options, args = parser.parse_args(args)
4865 if args:
4866 parser.error('Unrecognized args: %s' % ' '.join(args))
4867
4868 auth_config = auth.extract_auth_config_from_options(options)
4869 cl = Changelist(auth_config=auth_config)
4870 if not cl.GetIssue():
4871 parser.error('Need to upload first')
4872
tandrii221ab252016-10-06 08:12:04 -07004873 patchset = options.patchset
4874 if not patchset:
4875 patchset = cl.GetMostRecentPatchset()
4876 if not patchset:
4877 parser.error('Codereview doesn\'t know about issue %s. '
4878 'No access to issue or wrong issue number?\n'
4879 'Either upload first, or pass --patchset explicitely' %
4880 cl.GetIssue())
4881
4882 if patchset != cl.GetPatchset():
4883 print('WARNING: Mismatch between local config and server. Did a previous '
4884 'upload fail?\n'
4885 'By default, git cl try uses latest patchset from codereview.\n'
4886 'Continuing using patchset %s.\n' % patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004887 try:
tandrii221ab252016-10-06 08:12:04 -07004888 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004889 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004890 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004891 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004892 if options.json:
4893 write_try_results_json(options.json, jobs)
4894 else:
4895 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004896 return 0
4897
4898
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004899@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004900def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004901 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004902 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004903 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004904 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004905
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004906 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004907 if args:
4908 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004909 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004910 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004911 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004912 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004913
4914 # Clear configured merge-base, if there is one.
4915 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004916 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004917 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004918 return 0
4919
4920
thestig@chromium.org00858c82013-12-02 23:08:03 +00004921def CMDweb(parser, args):
4922 """Opens the current CL in the web browser."""
4923 _, args = parser.parse_args(args)
4924 if args:
4925 parser.error('Unrecognized args: %s' % ' '.join(args))
4926
4927 issue_url = Changelist().GetIssueURL()
4928 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004929 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004930 return 1
4931
4932 webbrowser.open(issue_url)
4933 return 0
4934
4935
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004936def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004937 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004938 parser.add_option('-d', '--dry-run', action='store_true',
4939 help='trigger in dry run mode')
4940 parser.add_option('-c', '--clear', action='store_true',
4941 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004942 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004943 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004944 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004945 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004946 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004947 if args:
4948 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004949 if options.dry_run and options.clear:
4950 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4951
iannuccie53c9352016-08-17 14:40:40 -07004952 cl = Changelist(auth_config=auth_config, issue=options.issue,
4953 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004954 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004955 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004956 elif options.dry_run:
4957 state = _CQState.DRY_RUN
4958 else:
4959 state = _CQState.COMMIT
4960 if not cl.GetIssue():
4961 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004962 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004963 return 0
4964
4965
groby@chromium.org411034a2013-02-26 15:12:01 +00004966def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004967 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004968 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004969 auth.add_auth_options(parser)
4970 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004971 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004972 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004973 if args:
4974 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004975 cl = Changelist(auth_config=auth_config, issue=options.issue,
4976 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004977 # Ensure there actually is an issue to close.
4978 cl.GetDescription()
4979 cl.CloseIssue()
4980 return 0
4981
4982
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004983def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004984 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004985 parser.add_option(
4986 '--stat',
4987 action='store_true',
4988 dest='stat',
4989 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004990 auth.add_auth_options(parser)
4991 options, args = parser.parse_args(args)
4992 auth_config = auth.extract_auth_config_from_options(options)
4993 if args:
4994 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004995
4996 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004997 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004998 # Staged changes would be committed along with the patch from last
4999 # upload, hence counted toward the "last upload" side in the final
5000 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005001 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005002 return 1
5003
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005004 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005005 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005006 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005007 if not issue:
5008 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005009 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005010 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005011
5012 # Create a new branch based on the merge-base
5013 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005014 # Clear cached branch in cl object, to avoid overwriting original CL branch
5015 # properties.
5016 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005017 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005018 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005019 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005020 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005021 return rtn
5022
wychen@chromium.org06928532015-02-03 02:11:29 +00005023 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005024 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005025 cmd = ['git', 'diff']
5026 if options.stat:
5027 cmd.append('--stat')
5028 cmd.extend([TMP_BRANCH, branch, '--'])
5029 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005030 finally:
5031 RunGit(['checkout', '-q', branch])
5032 RunGit(['branch', '-D', TMP_BRANCH])
5033
5034 return 0
5035
5036
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005037def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005038 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005039 parser.add_option(
5040 '--no-color',
5041 action='store_true',
5042 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005043 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005044 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005045 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005046
5047 author = RunGit(['config', 'user.email']).strip() or None
5048
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005049 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005050
5051 if args:
5052 if len(args) > 1:
5053 parser.error('Unknown args')
5054 base_branch = args[0]
5055 else:
5056 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005057 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005058
5059 change = cl.GetChange(base_branch, None)
5060 return owners_finder.OwnersFinder(
5061 [f.LocalPath() for f in
5062 cl.GetChange(base_branch, None).AffectedFiles()],
5063 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005064 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005065 disable_color=options.no_color).run()
5066
5067
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005068def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005069 """Generates a diff command."""
5070 # Generate diff for the current branch's changes.
5071 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5072 upstream_commit, '--' ]
5073
5074 if args:
5075 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005076 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005077 diff_cmd.append(arg)
5078 else:
5079 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005080
5081 return diff_cmd
5082
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005083def MatchingFileType(file_name, extensions):
5084 """Returns true if the file name ends with one of the given extensions."""
5085 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005086
enne@chromium.org555cfe42014-01-29 18:21:39 +00005087@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005088def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005089 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005090 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005091 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005092 parser.add_option('--full', action='store_true',
5093 help='Reformat the full content of all touched files')
5094 parser.add_option('--dry-run', action='store_true',
5095 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005096 parser.add_option('--python', action='store_true',
5097 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005098 parser.add_option('--diff', action='store_true',
5099 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005100 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005101
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005102 # git diff generates paths against the root of the repository. Change
5103 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005104 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005105 if rel_base_path:
5106 os.chdir(rel_base_path)
5107
digit@chromium.org29e47272013-05-17 17:01:46 +00005108 # Grab the merge-base commit, i.e. the upstream commit of the current
5109 # branch when it was created or the last time it was rebased. This is
5110 # to cover the case where the user may have called "git fetch origin",
5111 # moving the origin branch to a newer commit, but hasn't rebased yet.
5112 upstream_commit = None
5113 cl = Changelist()
5114 upstream_branch = cl.GetUpstreamBranch()
5115 if upstream_branch:
5116 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5117 upstream_commit = upstream_commit.strip()
5118
5119 if not upstream_commit:
5120 DieWithError('Could not find base commit for this branch. '
5121 'Are you in detached state?')
5122
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005123 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5124 diff_output = RunGit(changed_files_cmd)
5125 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005126 # Filter out files deleted by this CL
5127 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005128
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005129 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5130 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5131 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005132 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005133
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005134 top_dir = os.path.normpath(
5135 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5136
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005137 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5138 # formatted. This is used to block during the presubmit.
5139 return_value = 0
5140
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005141 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005142 # Locate the clang-format binary in the checkout
5143 try:
5144 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005145 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005146 DieWithError(e)
5147
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005148 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005149 cmd = [clang_format_tool]
5150 if not opts.dry_run and not opts.diff:
5151 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005152 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005153 if opts.diff:
5154 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005155 else:
5156 env = os.environ.copy()
5157 env['PATH'] = str(os.path.dirname(clang_format_tool))
5158 try:
5159 script = clang_format.FindClangFormatScriptInChromiumTree(
5160 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005161 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005162 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005163
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005164 cmd = [sys.executable, script, '-p0']
5165 if not opts.dry_run and not opts.diff:
5166 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005167
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005168 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5169 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005170
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005171 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5172 if opts.diff:
5173 sys.stdout.write(stdout)
5174 if opts.dry_run and len(stdout) > 0:
5175 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005176
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005177 # Similar code to above, but using yapf on .py files rather than clang-format
5178 # on C/C++ files
5179 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005180 yapf_tool = gclient_utils.FindExecutable('yapf')
5181 if yapf_tool is None:
5182 DieWithError('yapf not found in PATH')
5183
5184 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005185 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005186 cmd = [yapf_tool]
5187 if not opts.dry_run and not opts.diff:
5188 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005189 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005190 if opts.diff:
5191 sys.stdout.write(stdout)
5192 else:
5193 # TODO(sbc): yapf --lines mode still has some issues.
5194 # https://github.com/google/yapf/issues/154
5195 DieWithError('--python currently only works with --full')
5196
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005197 # Dart's formatter does not have the nice property of only operating on
5198 # modified chunks, so hard code full.
5199 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005200 try:
5201 command = [dart_format.FindDartFmtToolInChromiumTree()]
5202 if not opts.dry_run and not opts.diff:
5203 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005204 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005205
ppi@chromium.org6593d932016-03-03 15:41:15 +00005206 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005207 if opts.dry_run and stdout:
5208 return_value = 2
5209 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005210 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5211 'found in this checkout. Files in other languages are still '
5212 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005213
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005214 # Format GN build files. Always run on full build files for canonical form.
5215 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005216 cmd = ['gn', 'format' ]
5217 if opts.dry_run or opts.diff:
5218 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005219 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005220 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5221 shell=sys.platform == 'win32',
5222 cwd=top_dir)
5223 if opts.dry_run and gn_ret == 2:
5224 return_value = 2 # Not formatted.
5225 elif opts.diff and gn_ret == 2:
5226 # TODO this should compute and print the actual diff.
5227 print("This change has GN build file diff for " + gn_diff_file)
5228 elif gn_ret != 0:
5229 # For non-dry run cases (and non-2 return values for dry-run), a
5230 # nonzero error code indicates a failure, probably because the file
5231 # doesn't parse.
5232 DieWithError("gn format failed on " + gn_diff_file +
5233 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005234
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005235 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005236
5237
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005238@subcommand.usage('<codereview url or issue id>')
5239def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005240 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005241 _, args = parser.parse_args(args)
5242
5243 if len(args) != 1:
5244 parser.print_help()
5245 return 1
5246
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005247 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005248 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005249 parser.print_help()
5250 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005251 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005252
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005253 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005254 output = RunGit(['config', '--local', '--get-regexp',
5255 r'branch\..*\.%s' % issueprefix],
5256 error_ok=True)
5257 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005258 if issue == target_issue:
5259 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005260
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005261 branches = []
5262 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005263 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005264 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005265 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005266 return 1
5267 if len(branches) == 1:
5268 RunGit(['checkout', branches[0]])
5269 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005270 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005271 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005272 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005273 which = raw_input('Choose by index: ')
5274 try:
5275 RunGit(['checkout', branches[int(which)]])
5276 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005277 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005278 return 1
5279
5280 return 0
5281
5282
maruel@chromium.org29404b52014-09-08 22:58:00 +00005283def CMDlol(parser, args):
5284 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005285 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005286 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5287 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5288 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005289 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005290 return 0
5291
5292
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005293class OptionParser(optparse.OptionParser):
5294 """Creates the option parse and add --verbose support."""
5295 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005296 optparse.OptionParser.__init__(
5297 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005298 self.add_option(
5299 '-v', '--verbose', action='count', default=0,
5300 help='Use 2 times for more debugging info')
5301
5302 def parse_args(self, args=None, values=None):
5303 options, args = optparse.OptionParser.parse_args(self, args, values)
5304 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5305 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5306 return options, args
5307
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005308
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005309def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005310 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005311 print('\nYour python version %s is unsupported, please upgrade.\n' %
5312 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005313 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005314
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005315 # Reload settings.
5316 global settings
5317 settings = Settings()
5318
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005319 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005320 dispatcher = subcommand.CommandDispatcher(__name__)
5321 try:
5322 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005323 except auth.AuthenticationError as e:
5324 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005325 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005326 if e.code != 500:
5327 raise
5328 DieWithError(
5329 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5330 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005331 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005332
5333
5334if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005335 # These affect sys.stdout so do it outside of main() to simplify mocks in
5336 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005337 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005338 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005339 try:
5340 sys.exit(main(sys.argv[1:]))
5341 except KeyboardInterrupt:
5342 sys.stderr.write('interrupted\n')
5343 sys.exit(1)