blob: 3acfdbbdf5439b010bcfa42c242722820b24c036 [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']
179 if value_type == int:
180 args.append('--int')
181 elif value_type == bool:
182 args.append('--bool')
183 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']
205 # Check for boolean first, becuase bool is int, but int is not bool.
206 if value is None:
207 args.append('--unset')
208 elif isinstance(value, bool):
209 args.append('--bool')
210 value = str(value).lower()
211 elif isinstance(value, int):
212 args.append('--int')
213 value = str(value)
214 else:
215 value = str(value)
216 args.append(_git_branch_config_key(branch, key))
217 if value is not None:
218 args.append(value)
219 RunGit(args, **kwargs)
220
221
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000222def add_git_similarity(parser):
223 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700224 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000225 help='Sets the percentage that a pair of files need to match in order to'
226 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000227 parser.add_option(
228 '--find-copies', action='store_true',
229 help='Allows git to look for copies.')
230 parser.add_option(
231 '--no-find-copies', action='store_false', dest='find_copies',
232 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000233
234 old_parser_args = parser.parse_args
235 def Parse(args):
236 options, args = old_parser_args(args)
237
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000238 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700239 options.similarity = _git_get_branch_config_value(
240 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000241 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000242 print('Note: Saving similarity of %d%% in git config.'
243 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700244 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000245
iannucci@chromium.org79540052012-10-19 23:15:26 +0000246 options.similarity = max(0, min(options.similarity, 100))
247
248 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700249 options.find_copies = _git_get_branch_config_value(
250 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 else:
tandrii5d48c322016-08-18 16:19:37 -0700252 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000253
254 print('Using %d%% similarity for rename/copy detection. '
255 'Override with --similarity.' % options.similarity)
256
257 return options, args
258 parser.parse_args = Parse
259
260
machenbach@chromium.org45453142015-09-15 08:45:22 +0000261def _get_properties_from_options(options):
262 properties = dict(x.split('=', 1) for x in options.properties)
263 for key, val in properties.iteritems():
264 try:
265 properties[key] = json.loads(val)
266 except ValueError:
267 pass # If a value couldn't be evaluated, treat it as a string.
268 return properties
269
270
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000271def _prefix_master(master):
272 """Convert user-specified master name to full master name.
273
274 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
275 name, while the developers always use shortened master name
276 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
277 function does the conversion for buildbucket migration.
278 """
279 prefix = 'master.'
280 if master.startswith(prefix):
281 return master
282 return '%s%s' % (prefix, master)
283
284
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000285def _buildbucket_retry(operation_name, http, *args, **kwargs):
286 """Retries requests to buildbucket service and returns parsed json content."""
287 try_count = 0
288 while True:
289 response, content = http.request(*args, **kwargs)
290 try:
291 content_json = json.loads(content)
292 except ValueError:
293 content_json = None
294
295 # Buildbucket could return an error even if status==200.
296 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000297 error = content_json.get('error')
298 if error.get('code') == 403:
299 raise BuildbucketResponseException(
300 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000301 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000302 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000303 raise BuildbucketResponseException(msg)
304
305 if response.status == 200:
306 if not content_json:
307 raise BuildbucketResponseException(
308 'Buildbucket returns invalid json content: %s.\n'
309 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
310 content)
311 return content_json
312 if response.status < 500 or try_count >= 2:
313 raise httplib2.HttpLib2Error(content)
314
315 # status >= 500 means transient failures.
316 logging.debug('Transient errors when %s. Will retry.', operation_name)
317 time.sleep(0.5 + 1.5*try_count)
318 try_count += 1
319 assert False, 'unreachable'
320
321
machenbach@chromium.org45453142015-09-15 08:45:22 +0000322def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323 rietveld_url = settings.GetDefaultServerUrl()
324 rietveld_host = urlparse.urlparse(rietveld_url).hostname
325 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
326 http = authenticator.authorize(httplib2.Http())
327 http.force_exception_to_status_code = True
328 issue_props = changelist.GetIssueProperties()
329 issue = changelist.GetIssue()
330 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000331 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332
333 buildbucket_put_url = (
334 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000335 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
337 hostname=rietveld_host,
338 issue=issue,
339 patch=patchset)
340
341 batch_req_body = {'builds': []}
342 print_text = []
343 print_text.append('Tried jobs on:')
344 for master, builders_and_tests in sorted(masters.iteritems()):
345 print_text.append('Master: %s' % master)
346 bucket = _prefix_master(master)
347 for builder, tests in sorted(builders_and_tests.iteritems()):
348 print_text.append(' %s: %s' % (builder, tests))
349 parameters = {
350 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000351 'changes': [{
352 'author': {'email': issue_props['owner_email']},
353 'revision': options.revision,
354 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000355 'properties': {
356 'category': category,
357 'issue': issue,
358 'master': master,
359 'patch_project': issue_props['project'],
360 'patch_storage': 'rietveld',
361 'patchset': patchset,
362 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000364 },
365 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000366 if 'presubmit' in builder.lower():
367 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000368 if tests:
369 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000370 if properties:
371 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000372 if options.clobber:
373 parameters['properties']['clobber'] = True
374 batch_req_body['builds'].append(
375 {
376 'bucket': bucket,
377 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000378 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000379 'tags': ['builder:%s' % builder,
380 'buildset:%s' % buildset,
381 'master:%s' % master,
382 'user_agent:git_cl_try']
383 }
384 )
385
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000386 _buildbucket_retry(
387 'triggering tryjobs',
388 http,
389 buildbucket_put_url,
390 'PUT',
391 body=json.dumps(batch_req_body),
392 headers={'Content-Type': 'application/json'}
393 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000394 print_text.append('To see results here, run: git cl try-results')
395 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700396 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000397
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000398
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000399def fetch_try_jobs(auth_config, changelist, options):
400 """Fetches tryjobs from buildbucket.
401
402 Returns a map from build id to build info as json dictionary.
403 """
404 rietveld_url = settings.GetDefaultServerUrl()
405 rietveld_host = urlparse.urlparse(rietveld_url).hostname
406 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
407 if authenticator.has_cached_credentials():
408 http = authenticator.authorize(httplib2.Http())
409 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700410 print('Warning: Some results might be missing because %s' %
411 # Get the message on how to login.
412 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000413 http = httplib2.Http()
414
415 http.force_exception_to_status_code = True
416
417 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
418 hostname=rietveld_host,
419 issue=changelist.GetIssue(),
420 patch=options.patchset)
421 params = {'tag': 'buildset:%s' % buildset}
422
423 builds = {}
424 while True:
425 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
426 hostname=options.buildbucket_host,
427 params=urllib.urlencode(params))
428 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
429 for build in content.get('builds', []):
430 builds[build['id']] = build
431 if 'next_cursor' in content:
432 params['start_cursor'] = content['next_cursor']
433 else:
434 break
435 return builds
436
437
438def print_tryjobs(options, builds):
439 """Prints nicely result of fetch_try_jobs."""
440 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700441 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000442 return
443
444 # Make a copy, because we'll be modifying builds dictionary.
445 builds = builds.copy()
446 builder_names_cache = {}
447
448 def get_builder(b):
449 try:
450 return builder_names_cache[b['id']]
451 except KeyError:
452 try:
453 parameters = json.loads(b['parameters_json'])
454 name = parameters['builder_name']
455 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700456 print('WARNING: failed to get builder name for build %s: %s' % (
457 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000458 name = None
459 builder_names_cache[b['id']] = name
460 return name
461
462 def get_bucket(b):
463 bucket = b['bucket']
464 if bucket.startswith('master.'):
465 return bucket[len('master.'):]
466 return bucket
467
468 if options.print_master:
469 name_fmt = '%%-%ds %%-%ds' % (
470 max(len(str(get_bucket(b))) for b in builds.itervalues()),
471 max(len(str(get_builder(b))) for b in builds.itervalues()))
472 def get_name(b):
473 return name_fmt % (get_bucket(b), get_builder(b))
474 else:
475 name_fmt = '%%-%ds' % (
476 max(len(str(get_builder(b))) for b in builds.itervalues()))
477 def get_name(b):
478 return name_fmt % get_builder(b)
479
480 def sort_key(b):
481 return b['status'], b.get('result'), get_name(b), b.get('url')
482
483 def pop(title, f, color=None, **kwargs):
484 """Pop matching builds from `builds` dict and print them."""
485
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000486 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000487 colorize = str
488 else:
489 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
490
491 result = []
492 for b in builds.values():
493 if all(b.get(k) == v for k, v in kwargs.iteritems()):
494 builds.pop(b['id'])
495 result.append(b)
496 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700497 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000498 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700499 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000500
501 total = len(builds)
502 pop(status='COMPLETED', result='SUCCESS',
503 title='Successes:', color=Fore.GREEN,
504 f=lambda b: (get_name(b), b.get('url')))
505 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
506 title='Infra Failures:', color=Fore.MAGENTA,
507 f=lambda b: (get_name(b), b.get('url')))
508 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
509 title='Failures:', color=Fore.RED,
510 f=lambda b: (get_name(b), b.get('url')))
511 pop(status='COMPLETED', result='CANCELED',
512 title='Canceled:', color=Fore.MAGENTA,
513 f=lambda b: (get_name(b),))
514 pop(status='COMPLETED', result='FAILURE',
515 failure_reason='INVALID_BUILD_DEFINITION',
516 title='Wrong master/builder name:', color=Fore.MAGENTA,
517 f=lambda b: (get_name(b),))
518 pop(status='COMPLETED', result='FAILURE',
519 title='Other failures:',
520 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
521 pop(status='COMPLETED',
522 title='Other finished:',
523 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
524 pop(status='STARTED',
525 title='Started:', color=Fore.YELLOW,
526 f=lambda b: (get_name(b), b.get('url')))
527 pop(status='SCHEDULED',
528 title='Scheduled:',
529 f=lambda b: (get_name(b), 'id=%s' % b['id']))
530 # The last section is just in case buildbucket API changes OR there is a bug.
531 pop(title='Other:',
532 f=lambda b: (get_name(b), 'id=%s' % b['id']))
533 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700534 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535
536
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000537def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
538 """Return the corresponding git ref if |base_url| together with |glob_spec|
539 matches the full |url|.
540
541 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
542 """
543 fetch_suburl, as_ref = glob_spec.split(':')
544 if allow_wildcards:
545 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
546 if glob_match:
547 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
548 # "branches/{472,597,648}/src:refs/remotes/svn/*".
549 branch_re = re.escape(base_url)
550 if glob_match.group(1):
551 branch_re += '/' + re.escape(glob_match.group(1))
552 wildcard = glob_match.group(2)
553 if wildcard == '*':
554 branch_re += '([^/]*)'
555 else:
556 # Escape and replace surrounding braces with parentheses and commas
557 # with pipe symbols.
558 wildcard = re.escape(wildcard)
559 wildcard = re.sub('^\\\\{', '(', wildcard)
560 wildcard = re.sub('\\\\,', '|', wildcard)
561 wildcard = re.sub('\\\\}$', ')', wildcard)
562 branch_re += wildcard
563 if glob_match.group(3):
564 branch_re += re.escape(glob_match.group(3))
565 match = re.match(branch_re, url)
566 if match:
567 return re.sub('\*$', match.group(1), as_ref)
568
569 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
570 if fetch_suburl:
571 full_url = base_url + '/' + fetch_suburl
572 else:
573 full_url = base_url
574 if full_url == url:
575 return as_ref
576 return None
577
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000578
iannucci@chromium.org79540052012-10-19 23:15:26 +0000579def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000580 """Prints statistics about the change to the user."""
581 # --no-ext-diff is broken in some versions of Git, so try to work around
582 # this by overriding the environment (but there is still a problem if the
583 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000584 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000585 if 'GIT_EXTERNAL_DIFF' in env:
586 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000587
588 if find_copies:
589 similarity_options = ['--find-copies-harder', '-l100000',
590 '-C%s' % similarity]
591 else:
592 similarity_options = ['-M%s' % similarity]
593
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000594 try:
595 stdout = sys.stdout.fileno()
596 except AttributeError:
597 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000598 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000599 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000600 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000601 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000602
603
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000604class BuildbucketResponseException(Exception):
605 pass
606
607
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608class Settings(object):
609 def __init__(self):
610 self.default_server = None
611 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000612 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 self.is_git_svn = None
614 self.svn_branch = None
615 self.tree_status_url = None
616 self.viewvc_url = None
617 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000618 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000619 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000620 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000621 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000622 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000623 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000624 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625
626 def LazyUpdateIfNeeded(self):
627 """Updates the settings from a codereview.settings file, if available."""
628 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000629 # The only value that actually changes the behavior is
630 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000631 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000632 error_ok=True
633 ).strip().lower()
634
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000636 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 LoadCodereviewSettingsFromFile(cr_settings_file)
638 self.updated = True
639
640 def GetDefaultServerUrl(self, error_ok=False):
641 if not self.default_server:
642 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000643 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000644 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000645 if error_ok:
646 return self.default_server
647 if not self.default_server:
648 error_message = ('Could not find settings file. You must configure '
649 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000650 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000651 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 return self.default_server
653
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000654 @staticmethod
655 def GetRelativeRoot():
656 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000657
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000659 if self.root is None:
660 self.root = os.path.abspath(self.GetRelativeRoot())
661 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000662
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000663 def GetGitMirror(self, remote='origin'):
664 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000665 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000666 if not os.path.isdir(local_url):
667 return None
668 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
669 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
670 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
671 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
672 if mirror.exists():
673 return mirror
674 return None
675
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 def GetIsGitSvn(self):
677 """Return true if this repo looks like it's using git-svn."""
678 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000679 if self.GetPendingRefPrefix():
680 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
681 self.is_git_svn = False
682 else:
683 # If you have any "svn-remote.*" config keys, we think you're using svn.
684 self.is_git_svn = RunGitWithCode(
685 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686 return self.is_git_svn
687
688 def GetSVNBranch(self):
689 if self.svn_branch is None:
690 if not self.GetIsGitSvn():
691 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
692
693 # Try to figure out which remote branch we're based on.
694 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000695 # 1) iterate through our branch history and find the svn URL.
696 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697
698 # regexp matching the git-svn line that contains the URL.
699 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
700
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000701 # We don't want to go through all of history, so read a line from the
702 # pipe at a time.
703 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000704 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000705 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
706 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000707 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000708 for line in proc.stdout:
709 match = git_svn_re.match(line)
710 if match:
711 url = match.group(1)
712 proc.stdout.close() # Cut pipe.
713 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000714
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000715 if url:
716 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
717 remotes = RunGit(['config', '--get-regexp',
718 r'^svn-remote\..*\.url']).splitlines()
719 for remote in remotes:
720 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000722 remote = match.group(1)
723 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000724 rewrite_root = RunGit(
725 ['config', 'svn-remote.%s.rewriteRoot' % remote],
726 error_ok=True).strip()
727 if rewrite_root:
728 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000729 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000730 ['config', 'svn-remote.%s.fetch' % remote],
731 error_ok=True).strip()
732 if fetch_spec:
733 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
734 if self.svn_branch:
735 break
736 branch_spec = RunGit(
737 ['config', 'svn-remote.%s.branches' % remote],
738 error_ok=True).strip()
739 if branch_spec:
740 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
741 if self.svn_branch:
742 break
743 tag_spec = RunGit(
744 ['config', 'svn-remote.%s.tags' % remote],
745 error_ok=True).strip()
746 if tag_spec:
747 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
748 if self.svn_branch:
749 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750
751 if not self.svn_branch:
752 DieWithError('Can\'t guess svn branch -- try specifying it on the '
753 'command line')
754
755 return self.svn_branch
756
757 def GetTreeStatusUrl(self, error_ok=False):
758 if not self.tree_status_url:
759 error_message = ('You must configure your tree status URL by running '
760 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000761 self.tree_status_url = self._GetRietveldConfig(
762 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763 return self.tree_status_url
764
765 def GetViewVCUrl(self):
766 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000767 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 return self.viewvc_url
769
rmistry@google.com90752582014-01-14 21:04:50 +0000770 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000771 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000772
rmistry@google.com78948ed2015-07-08 23:09:57 +0000773 def GetIsSkipDependencyUpload(self, branch_name):
774 """Returns true if specified branch should skip dep uploads."""
775 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
776 error_ok=True)
777
rmistry@google.com5626a922015-02-26 14:03:30 +0000778 def GetRunPostUploadHook(self):
779 run_post_upload_hook = self._GetRietveldConfig(
780 'run-post-upload-hook', error_ok=True)
781 return run_post_upload_hook == "True"
782
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000783 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000784 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000785
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000786 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000787 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000788
ukai@chromium.orge8077812012-02-03 03:41:46 +0000789 def GetIsGerrit(self):
790 """Return true if this repo is assosiated with gerrit code review system."""
791 if self.is_gerrit is None:
792 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
793 return self.is_gerrit
794
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000795 def GetSquashGerritUploads(self):
796 """Return true if uploads to Gerrit should be squashed by default."""
797 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700798 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
799 if self.squash_gerrit_uploads is None:
800 # Default is squash now (http://crbug.com/611892#c23).
801 self.squash_gerrit_uploads = not (
802 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
803 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000804 return self.squash_gerrit_uploads
805
tandriia60502f2016-06-20 02:01:53 -0700806 def GetSquashGerritUploadsOverride(self):
807 """Return True or False if codereview.settings should be overridden.
808
809 Returns None if no override has been defined.
810 """
811 # See also http://crbug.com/611892#c23
812 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
813 error_ok=True).strip()
814 if result == 'true':
815 return True
816 if result == 'false':
817 return False
818 return None
819
tandrii@chromium.org28253532016-04-14 13:46:56 +0000820 def GetGerritSkipEnsureAuthenticated(self):
821 """Return True if EnsureAuthenticated should not be done for Gerrit
822 uploads."""
823 if self.gerrit_skip_ensure_authenticated is None:
824 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000825 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000826 error_ok=True).strip() == 'true')
827 return self.gerrit_skip_ensure_authenticated
828
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000829 def GetGitEditor(self):
830 """Return the editor specified in the git config, or None if none is."""
831 if self.git_editor is None:
832 self.git_editor = self._GetConfig('core.editor', error_ok=True)
833 return self.git_editor or None
834
thestig@chromium.org44202a22014-03-11 19:22:18 +0000835 def GetLintRegex(self):
836 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
837 DEFAULT_LINT_REGEX)
838
839 def GetLintIgnoreRegex(self):
840 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
841 DEFAULT_LINT_IGNORE_REGEX)
842
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000843 def GetProject(self):
844 if not self.project:
845 self.project = self._GetRietveldConfig('project', error_ok=True)
846 return self.project
847
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000848 def GetForceHttpsCommitUrl(self):
849 if not self.force_https_commit_url:
850 self.force_https_commit_url = self._GetRietveldConfig(
851 'force-https-commit-url', error_ok=True)
852 return self.force_https_commit_url
853
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000854 def GetPendingRefPrefix(self):
855 if not self.pending_ref_prefix:
856 self.pending_ref_prefix = self._GetRietveldConfig(
857 'pending-ref-prefix', error_ok=True)
858 return self.pending_ref_prefix
859
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000860 def _GetRietveldConfig(self, param, **kwargs):
861 return self._GetConfig('rietveld.' + param, **kwargs)
862
rmistry@google.com78948ed2015-07-08 23:09:57 +0000863 def _GetBranchConfig(self, branch_name, param, **kwargs):
864 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
865
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866 def _GetConfig(self, param, **kwargs):
867 self.LazyUpdateIfNeeded()
868 return RunGit(['config', param], **kwargs).strip()
869
870
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000871def ShortBranchName(branch):
872 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000873 return branch.replace('refs/heads/', '', 1)
874
875
876def GetCurrentBranchRef():
877 """Returns branch ref (e.g., refs/heads/master) or None."""
878 return RunGit(['symbolic-ref', 'HEAD'],
879 stderr=subprocess2.VOID, error_ok=True).strip() or None
880
881
882def GetCurrentBranch():
883 """Returns current branch or None.
884
885 For refs/heads/* branches, returns just last part. For others, full ref.
886 """
887 branchref = GetCurrentBranchRef()
888 if branchref:
889 return ShortBranchName(branchref)
890 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891
892
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000893class _CQState(object):
894 """Enum for states of CL with respect to Commit Queue."""
895 NONE = 'none'
896 DRY_RUN = 'dry_run'
897 COMMIT = 'commit'
898
899 ALL_STATES = [NONE, DRY_RUN, COMMIT]
900
901
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000902class _ParsedIssueNumberArgument(object):
903 def __init__(self, issue=None, patchset=None, hostname=None):
904 self.issue = issue
905 self.patchset = patchset
906 self.hostname = hostname
907
908 @property
909 def valid(self):
910 return self.issue is not None
911
912
913class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
914 def __init__(self, *args, **kwargs):
915 self.patch_url = kwargs.pop('patch_url', None)
916 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
917
918
919def ParseIssueNumberArgument(arg):
920 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
921 fail_result = _ParsedIssueNumberArgument()
922
923 if arg.isdigit():
924 return _ParsedIssueNumberArgument(issue=int(arg))
925 if not arg.startswith('http'):
926 return fail_result
927 url = gclient_utils.UpgradeToHttps(arg)
928 try:
929 parsed_url = urlparse.urlparse(url)
930 except ValueError:
931 return fail_result
932 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
933 tmp = cls.ParseIssueURL(parsed_url)
934 if tmp is not None:
935 return tmp
936 return fail_result
937
938
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000940 """Changelist works with one changelist in local branch.
941
942 Supports two codereview backends: Rietveld or Gerrit, selected at object
943 creation.
944
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000945 Notes:
946 * Not safe for concurrent multi-{thread,process} use.
947 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700948 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000949 """
950
951 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
952 """Create a new ChangeList instance.
953
954 If issue is given, the codereview must be given too.
955
956 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
957 Otherwise, it's decided based on current configuration of the local branch,
958 with default being 'rietveld' for backwards compatibility.
959 See _load_codereview_impl for more details.
960
961 **kwargs will be passed directly to codereview implementation.
962 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000964 global settings
965 if not settings:
966 # Happens when git_cl.py is used as a utility library.
967 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000968
969 if issue:
970 assert codereview, 'codereview must be known, if issue is known'
971
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 self.branchref = branchref
973 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000974 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975 self.branch = ShortBranchName(self.branchref)
976 else:
977 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000979 self.lookedup_issue = False
980 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981 self.has_description = False
982 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000983 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000985 self.cc = None
986 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000987 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000988
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000989 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000990 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000991 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000992 assert self._codereview_impl
993 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000994
995 def _load_codereview_impl(self, codereview=None, **kwargs):
996 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000997 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
998 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
999 self._codereview = codereview
1000 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001001 return
1002
1003 # Automatic selection based on issue number set for a current branch.
1004 # Rietveld takes precedence over Gerrit.
1005 assert not self.issue
1006 # Whether we find issue or not, we are doing the lookup.
1007 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001008 if self.GetBranch():
1009 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1010 issue = _git_get_branch_config_value(
1011 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1012 if issue:
1013 self._codereview = codereview
1014 self._codereview_impl = cls(self, **kwargs)
1015 self.issue = int(issue)
1016 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001017
1018 # No issue is set for this branch, so decide based on repo-wide settings.
1019 return self._load_codereview_impl(
1020 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1021 **kwargs)
1022
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001023 def IsGerrit(self):
1024 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001025
1026 def GetCCList(self):
1027 """Return the users cc'd on this CL.
1028
1029 Return is a string suitable for passing to gcl with the --cc flag.
1030 """
1031 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001032 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001033 more_cc = ','.join(self.watchers)
1034 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1035 return self.cc
1036
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001037 def GetCCListWithoutDefault(self):
1038 """Return the users cc'd on this CL excluding default ones."""
1039 if self.cc is None:
1040 self.cc = ','.join(self.watchers)
1041 return self.cc
1042
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001043 def SetWatchers(self, watchers):
1044 """Set the list of email addresses that should be cc'd based on the changed
1045 files in this CL.
1046 """
1047 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048
1049 def GetBranch(self):
1050 """Returns the short branch name, e.g. 'master'."""
1051 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001052 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001053 if not branchref:
1054 return None
1055 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 self.branch = ShortBranchName(self.branchref)
1057 return self.branch
1058
1059 def GetBranchRef(self):
1060 """Returns the full branch name, e.g. 'refs/heads/master'."""
1061 self.GetBranch() # Poke the lazy loader.
1062 return self.branchref
1063
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001064 def ClearBranch(self):
1065 """Clears cached branch data of this object."""
1066 self.branch = self.branchref = None
1067
tandrii5d48c322016-08-18 16:19:37 -07001068 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1069 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1070 kwargs['branch'] = self.GetBranch()
1071 return _git_get_branch_config_value(key, default, **kwargs)
1072
1073 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1074 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1075 assert self.GetBranch(), (
1076 'this CL must have an associated branch to %sset %s%s' %
1077 ('un' if value is None else '',
1078 key,
1079 '' if value is None else ' to %r' % value))
1080 kwargs['branch'] = self.GetBranch()
1081 return _git_set_branch_config_value(key, value, **kwargs)
1082
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001083 @staticmethod
1084 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001085 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086 e.g. 'origin', 'refs/heads/master'
1087 """
1088 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001089 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001091 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001092 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001094 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1095 error_ok=True).strip()
1096 if upstream_branch:
1097 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001099 # Fall back on trying a git-svn upstream branch.
1100 if settings.GetIsGitSvn():
1101 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001103 # Else, try to guess the origin remote.
1104 remote_branches = RunGit(['branch', '-r']).split()
1105 if 'origin/master' in remote_branches:
1106 # Fall back on origin/master if it exits.
1107 remote = 'origin'
1108 upstream_branch = 'refs/heads/master'
1109 elif 'origin/trunk' in remote_branches:
1110 # Fall back on origin/trunk if it exists. Generally a shared
1111 # git-svn clone
1112 remote = 'origin'
1113 upstream_branch = 'refs/heads/trunk'
1114 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115 DieWithError(
1116 'Unable to determine default branch to diff against.\n'
1117 'Either pass complete "git diff"-style arguments, like\n'
1118 ' git cl upload origin/master\n'
1119 'or verify this branch is set up to track another \n'
1120 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121
1122 return remote, upstream_branch
1123
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001124 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001125 upstream_branch = self.GetUpstreamBranch()
1126 if not BranchExists(upstream_branch):
1127 DieWithError('The upstream for the current branch (%s) does not exist '
1128 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001129 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001130 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001131
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 def GetUpstreamBranch(self):
1133 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001134 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001136 upstream_branch = upstream_branch.replace('refs/heads/',
1137 'refs/remotes/%s/' % remote)
1138 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1139 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 self.upstream_branch = upstream_branch
1141 return self.upstream_branch
1142
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001143 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001144 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001145 remote, branch = None, self.GetBranch()
1146 seen_branches = set()
1147 while branch not in seen_branches:
1148 seen_branches.add(branch)
1149 remote, branch = self.FetchUpstreamTuple(branch)
1150 branch = ShortBranchName(branch)
1151 if remote != '.' or branch.startswith('refs/remotes'):
1152 break
1153 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001154 remotes = RunGit(['remote'], error_ok=True).split()
1155 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001156 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001157 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001158 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001159 logging.warning('Could not determine which remote this change is '
1160 'associated with, so defaulting to "%s". This may '
1161 'not be what you want. You may prevent this message '
1162 'by running "git svn info" as documented here: %s',
1163 self._remote,
1164 GIT_INSTRUCTIONS_URL)
1165 else:
1166 logging.warn('Could not determine which remote this change is '
1167 'associated with. You may prevent this message by '
1168 'running "git svn info" as documented here: %s',
1169 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001170 branch = 'HEAD'
1171 if branch.startswith('refs/remotes'):
1172 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001173 elif branch.startswith('refs/branch-heads/'):
1174 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001175 else:
1176 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001177 return self._remote
1178
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001179 def GitSanityChecks(self, upstream_git_obj):
1180 """Checks git repo status and ensures diff is from local commits."""
1181
sbc@chromium.org79706062015-01-14 21:18:12 +00001182 if upstream_git_obj is None:
1183 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001184 print('ERROR: unable to determine current branch (detached HEAD?)',
1185 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001186 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001187 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001188 return False
1189
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001190 # Verify the commit we're diffing against is in our current branch.
1191 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1192 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1193 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001194 print('ERROR: %s is not in the current branch. You may need to rebase '
1195 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001196 return False
1197
1198 # List the commits inside the diff, and verify they are all local.
1199 commits_in_diff = RunGit(
1200 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1201 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1202 remote_branch = remote_branch.strip()
1203 if code != 0:
1204 _, remote_branch = self.GetRemoteBranch()
1205
1206 commits_in_remote = RunGit(
1207 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1208
1209 common_commits = set(commits_in_diff) & set(commits_in_remote)
1210 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001211 print('ERROR: Your diff contains %d commits already in %s.\n'
1212 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1213 'the diff. If you are using a custom git flow, you can override'
1214 ' the reference used for this check with "git config '
1215 'gitcl.remotebranch <git-ref>".' % (
1216 len(common_commits), remote_branch, upstream_git_obj),
1217 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001218 return False
1219 return True
1220
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001221 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001222 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001223
1224 Returns None if it is not set.
1225 """
tandrii5d48c322016-08-18 16:19:37 -07001226 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001227
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001228 def GetGitSvnRemoteUrl(self):
1229 """Return the configured git-svn remote URL parsed from git svn info.
1230
1231 Returns None if it is not set.
1232 """
1233 # URL is dependent on the current directory.
1234 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1235 if data:
1236 keys = dict(line.split(': ', 1) for line in data.splitlines()
1237 if ': ' in line)
1238 return keys.get('URL', None)
1239 return None
1240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 def GetRemoteUrl(self):
1242 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1243
1244 Returns None if there is no remote.
1245 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001246 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001247 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1248
1249 # If URL is pointing to a local directory, it is probably a git cache.
1250 if os.path.isdir(url):
1251 url = RunGit(['config', 'remote.%s.url' % remote],
1252 error_ok=True,
1253 cwd=url).strip()
1254 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001256 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001257 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001258 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001259 self.issue = self._GitGetBranchConfigValue(
1260 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001261 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 return self.issue
1263
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 def GetIssueURL(self):
1265 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001266 issue = self.GetIssue()
1267 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001268 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001269 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270
1271 def GetDescription(self, pretty=False):
1272 if not self.has_description:
1273 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001274 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275 self.has_description = True
1276 if pretty:
1277 wrapper = textwrap.TextWrapper()
1278 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1279 return wrapper.fill(self.description)
1280 return self.description
1281
1282 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001283 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001284 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001285 self.patchset = self._GitGetBranchConfigValue(
1286 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001287 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 return self.patchset
1289
1290 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001291 """Set this branch's patchset. If patchset=0, clears the patchset."""
1292 assert self.GetBranch()
1293 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001294 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001295 else:
1296 self.patchset = int(patchset)
1297 self._GitSetBranchConfigValue(
1298 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001300 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001301 """Set this branch's issue. If issue isn't given, clears the issue."""
1302 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001304 issue = int(issue)
1305 self._GitSetBranchConfigValue(
1306 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001307 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001308 codereview_server = self._codereview_impl.GetCodereviewServer()
1309 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001310 self._GitSetBranchConfigValue(
1311 self._codereview_impl.CodereviewServerConfigKey(),
1312 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 else:
tandrii5d48c322016-08-18 16:19:37 -07001314 # Reset all of these just to be clean.
1315 reset_suffixes = [
1316 'last-upload-hash',
1317 self._codereview_impl.IssueConfigKey(),
1318 self._codereview_impl.PatchsetConfigKey(),
1319 self._codereview_impl.CodereviewServerConfigKey(),
1320 ] + self._PostUnsetIssueProperties()
1321 for prop in reset_suffixes:
1322 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001323 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001324 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001326 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 if not self.GitSanityChecks(upstream_branch):
1328 DieWithError('\nGit sanity check failure')
1329
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001330 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001331 if not root:
1332 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001333 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001334
1335 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001336 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001337 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001338 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001339 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001340 except subprocess2.CalledProcessError:
1341 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001342 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001343 'This branch probably doesn\'t exist anymore. To reset the\n'
1344 'tracking branch, please run\n'
1345 ' git branch --set-upstream %s trunk\n'
1346 'replacing trunk with origin/master or the relevant branch') %
1347 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001348
maruel@chromium.org52424302012-08-29 15:14:30 +00001349 issue = self.GetIssue()
1350 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001351 if issue:
1352 description = self.GetDescription()
1353 else:
1354 # If the change was never uploaded, use the log messages of all commits
1355 # up to the branch point, as git cl upload will prefill the description
1356 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001357 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1358 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001359
1360 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001361 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001362 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001363 name,
1364 description,
1365 absroot,
1366 files,
1367 issue,
1368 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001369 author,
1370 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001371
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001372 def UpdateDescription(self, description):
1373 self.description = description
1374 return self._codereview_impl.UpdateDescriptionRemote(description)
1375
1376 def RunHook(self, committing, may_prompt, verbose, change):
1377 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1378 try:
1379 return presubmit_support.DoPresubmitChecks(change, committing,
1380 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1381 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001382 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1383 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001384 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001385 DieWithError(
1386 ('%s\nMaybe your depot_tools is out of date?\n'
1387 'If all fails, contact maruel@') % e)
1388
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001389 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1390 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001391 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1392 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001393 else:
1394 # Assume url.
1395 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1396 urlparse.urlparse(issue_arg))
1397 if not parsed_issue_arg or not parsed_issue_arg.valid:
1398 DieWithError('Failed to parse issue argument "%s". '
1399 'Must be an issue number or a valid URL.' % issue_arg)
1400 return self._codereview_impl.CMDPatchWithParsedIssue(
1401 parsed_issue_arg, reject, nocommit, directory)
1402
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001403 def CMDUpload(self, options, git_diff_args, orig_args):
1404 """Uploads a change to codereview."""
1405 if git_diff_args:
1406 # TODO(ukai): is it ok for gerrit case?
1407 base_branch = git_diff_args[0]
1408 else:
1409 if self.GetBranch() is None:
1410 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1411
1412 # Default to diffing against common ancestor of upstream branch
1413 base_branch = self.GetCommonAncestorWithUpstream()
1414 git_diff_args = [base_branch, 'HEAD']
1415
1416 # Make sure authenticated to codereview before running potentially expensive
1417 # hooks. It is a fast, best efforts check. Codereview still can reject the
1418 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001419 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001420
1421 # Apply watchlists on upload.
1422 change = self.GetChange(base_branch, None)
1423 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1424 files = [f.LocalPath() for f in change.AffectedFiles()]
1425 if not options.bypass_watchlists:
1426 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1427
1428 if not options.bypass_hooks:
1429 if options.reviewers or options.tbr_owners:
1430 # Set the reviewer list now so that presubmit checks can access it.
1431 change_description = ChangeDescription(change.FullDescriptionText())
1432 change_description.update_reviewers(options.reviewers,
1433 options.tbr_owners,
1434 change)
1435 change.SetDescriptionText(change_description.description)
1436 hook_results = self.RunHook(committing=False,
1437 may_prompt=not options.force,
1438 verbose=options.verbose,
1439 change=change)
1440 if not hook_results.should_continue():
1441 return 1
1442 if not options.reviewers and hook_results.reviewers:
1443 options.reviewers = hook_results.reviewers.split(',')
1444
1445 if self.GetIssue():
1446 latest_patchset = self.GetMostRecentPatchset()
1447 local_patchset = self.GetPatchset()
1448 if (latest_patchset and local_patchset and
1449 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001450 print('The last upload made from this repository was patchset #%d but '
1451 'the most recent patchset on the server is #%d.'
1452 % (local_patchset, latest_patchset))
1453 print('Uploading will still work, but if you\'ve uploaded to this '
1454 'issue from another machine or branch the patch you\'re '
1455 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001456 ask_for_data('About to upload; enter to confirm.')
1457
1458 print_stats(options.similarity, options.find_copies, git_diff_args)
1459 ret = self.CMDUploadChange(options, git_diff_args, change)
1460 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001461 if options.use_commit_queue:
1462 self.SetCQState(_CQState.COMMIT)
1463 elif options.cq_dry_run:
1464 self.SetCQState(_CQState.DRY_RUN)
1465
tandrii5d48c322016-08-18 16:19:37 -07001466 _git_set_branch_config_value('last-upload-hash',
1467 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001468 # Run post upload hooks, if specified.
1469 if settings.GetRunPostUploadHook():
1470 presubmit_support.DoPostUploadExecuter(
1471 change,
1472 self,
1473 settings.GetRoot(),
1474 options.verbose,
1475 sys.stdout)
1476
1477 # Upload all dependencies if specified.
1478 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001479 print()
1480 print('--dependencies has been specified.')
1481 print('All dependent local branches will be re-uploaded.')
1482 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001483 # Remove the dependencies flag from args so that we do not end up in a
1484 # loop.
1485 orig_args.remove('--dependencies')
1486 ret = upload_branch_deps(self, orig_args)
1487 return ret
1488
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001489 def SetCQState(self, new_state):
1490 """Update the CQ state for latest patchset.
1491
1492 Issue must have been already uploaded and known.
1493 """
1494 assert new_state in _CQState.ALL_STATES
1495 assert self.GetIssue()
1496 return self._codereview_impl.SetCQState(new_state)
1497
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001498 # Forward methods to codereview specific implementation.
1499
1500 def CloseIssue(self):
1501 return self._codereview_impl.CloseIssue()
1502
1503 def GetStatus(self):
1504 return self._codereview_impl.GetStatus()
1505
1506 def GetCodereviewServer(self):
1507 return self._codereview_impl.GetCodereviewServer()
1508
1509 def GetApprovingReviewers(self):
1510 return self._codereview_impl.GetApprovingReviewers()
1511
1512 def GetMostRecentPatchset(self):
1513 return self._codereview_impl.GetMostRecentPatchset()
1514
1515 def __getattr__(self, attr):
1516 # This is because lots of untested code accesses Rietveld-specific stuff
1517 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001518 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001519 # Note that child method defines __getattr__ as well, and forwards it here,
1520 # because _RietveldChangelistImpl is not cleaned up yet, and given
1521 # deprecation of Rietveld, it should probably be just removed.
1522 # Until that time, avoid infinite recursion by bypassing __getattr__
1523 # of implementation class.
1524 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001525
1526
1527class _ChangelistCodereviewBase(object):
1528 """Abstract base class encapsulating codereview specifics of a changelist."""
1529 def __init__(self, changelist):
1530 self._changelist = changelist # instance of Changelist
1531
1532 def __getattr__(self, attr):
1533 # Forward methods to changelist.
1534 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1535 # _RietveldChangelistImpl to avoid this hack?
1536 return getattr(self._changelist, attr)
1537
1538 def GetStatus(self):
1539 """Apply a rough heuristic to give a simple summary of an issue's review
1540 or CQ status, assuming adherence to a common workflow.
1541
1542 Returns None if no issue for this branch, or specific string keywords.
1543 """
1544 raise NotImplementedError()
1545
1546 def GetCodereviewServer(self):
1547 """Returns server URL without end slash, like "https://codereview.com"."""
1548 raise NotImplementedError()
1549
1550 def FetchDescription(self):
1551 """Fetches and returns description from the codereview server."""
1552 raise NotImplementedError()
1553
tandrii5d48c322016-08-18 16:19:37 -07001554 @classmethod
1555 def IssueConfigKey(cls):
1556 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001557 raise NotImplementedError()
1558
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001559 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001560 def PatchsetConfigKey(cls):
1561 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001562 raise NotImplementedError()
1563
tandrii5d48c322016-08-18 16:19:37 -07001564 @classmethod
1565 def CodereviewServerConfigKey(cls):
1566 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567 raise NotImplementedError()
1568
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001569 def _PostUnsetIssueProperties(self):
1570 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001571 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001572
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001573 def GetRieveldObjForPresubmit(self):
1574 # This is an unfortunate Rietveld-embeddedness in presubmit.
1575 # For non-Rietveld codereviews, this probably should return a dummy object.
1576 raise NotImplementedError()
1577
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001578 def GetGerritObjForPresubmit(self):
1579 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1580 return None
1581
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001582 def UpdateDescriptionRemote(self, description):
1583 """Update the description on codereview site."""
1584 raise NotImplementedError()
1585
1586 def CloseIssue(self):
1587 """Closes the issue."""
1588 raise NotImplementedError()
1589
1590 def GetApprovingReviewers(self):
1591 """Returns a list of reviewers approving the change.
1592
1593 Note: not necessarily committers.
1594 """
1595 raise NotImplementedError()
1596
1597 def GetMostRecentPatchset(self):
1598 """Returns the most recent patchset number from the codereview site."""
1599 raise NotImplementedError()
1600
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001601 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1602 directory):
1603 """Fetches and applies the issue.
1604
1605 Arguments:
1606 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1607 reject: if True, reject the failed patch instead of switching to 3-way
1608 merge. Rietveld only.
1609 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1610 only.
1611 directory: switch to directory before applying the patch. Rietveld only.
1612 """
1613 raise NotImplementedError()
1614
1615 @staticmethod
1616 def ParseIssueURL(parsed_url):
1617 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1618 failed."""
1619 raise NotImplementedError()
1620
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001621 def EnsureAuthenticated(self, force):
1622 """Best effort check that user is authenticated with codereview server.
1623
1624 Arguments:
1625 force: whether to skip confirmation questions.
1626 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 raise NotImplementedError()
1628
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001629 def CMDUploadChange(self, options, args, change):
1630 """Uploads a change to codereview."""
1631 raise NotImplementedError()
1632
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001633 def SetCQState(self, new_state):
1634 """Update the CQ state for latest patchset.
1635
1636 Issue must have been already uploaded and known.
1637 """
1638 raise NotImplementedError()
1639
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001640
1641class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1642 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1643 super(_RietveldChangelistImpl, self).__init__(changelist)
1644 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001645 if not rietveld_server:
1646 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001647
1648 self._rietveld_server = rietveld_server
1649 self._auth_config = auth_config
1650 self._props = None
1651 self._rpc_server = None
1652
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001653 def GetCodereviewServer(self):
1654 if not self._rietveld_server:
1655 # If we're on a branch then get the server potentially associated
1656 # with that branch.
1657 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001658 self._rietveld_server = gclient_utils.UpgradeToHttps(
1659 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001660 if not self._rietveld_server:
1661 self._rietveld_server = settings.GetDefaultServerUrl()
1662 return self._rietveld_server
1663
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001664 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001665 """Best effort check that user is authenticated with Rietveld server."""
1666 if self._auth_config.use_oauth2:
1667 authenticator = auth.get_authenticator_for_host(
1668 self.GetCodereviewServer(), self._auth_config)
1669 if not authenticator.has_cached_credentials():
1670 raise auth.LoginRequiredError(self.GetCodereviewServer())
1671
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001672 def FetchDescription(self):
1673 issue = self.GetIssue()
1674 assert issue
1675 try:
1676 return self.RpcServer().get_description(issue).strip()
1677 except urllib2.HTTPError as e:
1678 if e.code == 404:
1679 DieWithError(
1680 ('\nWhile fetching the description for issue %d, received a '
1681 '404 (not found)\n'
1682 'error. It is likely that you deleted this '
1683 'issue on the server. If this is the\n'
1684 'case, please run\n\n'
1685 ' git cl issue 0\n\n'
1686 'to clear the association with the deleted issue. Then run '
1687 'this command again.') % issue)
1688 else:
1689 DieWithError(
1690 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1691 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001692 print('Warning: Failed to retrieve CL description due to network '
1693 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001694 return ''
1695
1696 def GetMostRecentPatchset(self):
1697 return self.GetIssueProperties()['patchsets'][-1]
1698
1699 def GetPatchSetDiff(self, issue, patchset):
1700 return self.RpcServer().get(
1701 '/download/issue%s_%s.diff' % (issue, patchset))
1702
1703 def GetIssueProperties(self):
1704 if self._props is None:
1705 issue = self.GetIssue()
1706 if not issue:
1707 self._props = {}
1708 else:
1709 self._props = self.RpcServer().get_issue_properties(issue, True)
1710 return self._props
1711
1712 def GetApprovingReviewers(self):
1713 return get_approving_reviewers(self.GetIssueProperties())
1714
1715 def AddComment(self, message):
1716 return self.RpcServer().add_comment(self.GetIssue(), message)
1717
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001718 def GetStatus(self):
1719 """Apply a rough heuristic to give a simple summary of an issue's review
1720 or CQ status, assuming adherence to a common workflow.
1721
1722 Returns None if no issue for this branch, or one of the following keywords:
1723 * 'error' - error from review tool (including deleted issues)
1724 * 'unsent' - not sent for review
1725 * 'waiting' - waiting for review
1726 * 'reply' - waiting for owner to reply to review
1727 * 'lgtm' - LGTM from at least one approved reviewer
1728 * 'commit' - in the commit queue
1729 * 'closed' - closed
1730 """
1731 if not self.GetIssue():
1732 return None
1733
1734 try:
1735 props = self.GetIssueProperties()
1736 except urllib2.HTTPError:
1737 return 'error'
1738
1739 if props.get('closed'):
1740 # Issue is closed.
1741 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001742 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001743 # Issue is in the commit queue.
1744 return 'commit'
1745
1746 try:
1747 reviewers = self.GetApprovingReviewers()
1748 except urllib2.HTTPError:
1749 return 'error'
1750
1751 if reviewers:
1752 # Was LGTM'ed.
1753 return 'lgtm'
1754
1755 messages = props.get('messages') or []
1756
tandrii9d2c7a32016-06-22 03:42:45 -07001757 # Skip CQ messages that don't require owner's action.
1758 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1759 if 'Dry run:' in messages[-1]['text']:
1760 messages.pop()
1761 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1762 # This message always follows prior messages from CQ,
1763 # so skip this too.
1764 messages.pop()
1765 else:
1766 # This is probably a CQ messages warranting user attention.
1767 break
1768
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001769 if not messages:
1770 # No message was sent.
1771 return 'unsent'
1772 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001773 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001774 return 'reply'
1775 return 'waiting'
1776
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001777 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001778 return self.RpcServer().update_description(
1779 self.GetIssue(), self.description)
1780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001782 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001783
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001784 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001785 return self.SetFlags({flag: value})
1786
1787 def SetFlags(self, flags):
1788 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001789 """
phajdan.jr68598232016-08-10 03:28:28 -07001790 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001791 try:
tandrii4b233bd2016-07-06 03:50:29 -07001792 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001793 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001794 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001795 if e.code == 404:
1796 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1797 if e.code == 403:
1798 DieWithError(
1799 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001800 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001801 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001802
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001803 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001804 """Returns an upload.RpcServer() to access this review's rietveld instance.
1805 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001806 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001807 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001809 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001810 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001812 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001813 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001814 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815
tandrii5d48c322016-08-18 16:19:37 -07001816 @classmethod
1817 def PatchsetConfigKey(cls):
1818 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819
tandrii5d48c322016-08-18 16:19:37 -07001820 @classmethod
1821 def CodereviewServerConfigKey(cls):
1822 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001823
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824 def GetRieveldObjForPresubmit(self):
1825 return self.RpcServer()
1826
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001827 def SetCQState(self, new_state):
1828 props = self.GetIssueProperties()
1829 if props.get('private'):
1830 DieWithError('Cannot set-commit on private issue')
1831
1832 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001833 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001834 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001835 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001836 else:
tandrii4b233bd2016-07-06 03:50:29 -07001837 assert new_state == _CQState.DRY_RUN
1838 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001839
1840
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001841 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1842 directory):
1843 # TODO(maruel): Use apply_issue.py
1844
1845 # PatchIssue should never be called with a dirty tree. It is up to the
1846 # caller to check this, but just in case we assert here since the
1847 # consequences of the caller not checking this could be dire.
1848 assert(not git_common.is_dirty_git_tree('apply'))
1849 assert(parsed_issue_arg.valid)
1850 self._changelist.issue = parsed_issue_arg.issue
1851 if parsed_issue_arg.hostname:
1852 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1853
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001854 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1855 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001856 assert parsed_issue_arg.patchset
1857 patchset = parsed_issue_arg.patchset
1858 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1859 else:
1860 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1861 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1862
1863 # Switch up to the top-level directory, if necessary, in preparation for
1864 # applying the patch.
1865 top = settings.GetRelativeRoot()
1866 if top:
1867 os.chdir(top)
1868
1869 # Git patches have a/ at the beginning of source paths. We strip that out
1870 # with a sed script rather than the -p flag to patch so we can feed either
1871 # Git or svn-style patches into the same apply command.
1872 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1873 try:
1874 patch_data = subprocess2.check_output(
1875 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1876 except subprocess2.CalledProcessError:
1877 DieWithError('Git patch mungling failed.')
1878 logging.info(patch_data)
1879
1880 # We use "git apply" to apply the patch instead of "patch" so that we can
1881 # pick up file adds.
1882 # The --index flag means: also insert into the index (so we catch adds).
1883 cmd = ['git', 'apply', '--index', '-p0']
1884 if directory:
1885 cmd.extend(('--directory', directory))
1886 if reject:
1887 cmd.append('--reject')
1888 elif IsGitVersionAtLeast('1.7.12'):
1889 cmd.append('--3way')
1890 try:
1891 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1892 stdin=patch_data, stdout=subprocess2.VOID)
1893 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001894 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001895 return 1
1896
1897 # If we had an issue, commit the current state and register the issue.
1898 if not nocommit:
1899 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1900 'patch from issue %(i)s at patchset '
1901 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1902 % {'i': self.GetIssue(), 'p': patchset})])
1903 self.SetIssue(self.GetIssue())
1904 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001905 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001906 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001907 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001908 return 0
1909
1910 @staticmethod
1911 def ParseIssueURL(parsed_url):
1912 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1913 return None
wychen3c1c1722016-08-04 11:46:36 -07001914 # Rietveld patch: https://domain/<number>/#ps<patchset>
1915 match = re.match(r'/(\d+)/$', parsed_url.path)
1916 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1917 if match and match2:
1918 return _RietveldParsedIssueNumberArgument(
1919 issue=int(match.group(1)),
1920 patchset=int(match2.group(1)),
1921 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001922 # Typical url: https://domain/<issue_number>[/[other]]
1923 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1924 if match:
1925 return _RietveldParsedIssueNumberArgument(
1926 issue=int(match.group(1)),
1927 hostname=parsed_url.netloc)
1928 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1929 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1930 if match:
1931 return _RietveldParsedIssueNumberArgument(
1932 issue=int(match.group(1)),
1933 patchset=int(match.group(2)),
1934 hostname=parsed_url.netloc,
1935 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1936 return None
1937
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001938 def CMDUploadChange(self, options, args, change):
1939 """Upload the patch to Rietveld."""
1940 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1941 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001942 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1943 if options.emulate_svn_auto_props:
1944 upload_args.append('--emulate_svn_auto_props')
1945
1946 change_desc = None
1947
1948 if options.email is not None:
1949 upload_args.extend(['--email', options.email])
1950
1951 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001952 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001953 upload_args.extend(['--title', options.title])
1954 if options.message:
1955 upload_args.extend(['--message', options.message])
1956 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001957 print('This branch is associated with issue %s. '
1958 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001959 else:
nodirca166002016-06-27 10:59:51 -07001960 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001961 upload_args.extend(['--title', options.title])
1962 message = (options.title or options.message or
1963 CreateDescriptionFromLog(args))
1964 change_desc = ChangeDescription(message)
1965 if options.reviewers or options.tbr_owners:
1966 change_desc.update_reviewers(options.reviewers,
1967 options.tbr_owners,
1968 change)
1969 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001970 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001971
1972 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001973 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001974 return 1
1975
1976 upload_args.extend(['--message', change_desc.description])
1977 if change_desc.get_reviewers():
1978 upload_args.append('--reviewers=%s' % ','.join(
1979 change_desc.get_reviewers()))
1980 if options.send_mail:
1981 if not change_desc.get_reviewers():
1982 DieWithError("Must specify reviewers to send email.")
1983 upload_args.append('--send_mail')
1984
1985 # We check this before applying rietveld.private assuming that in
1986 # rietveld.cc only addresses which we can send private CLs to are listed
1987 # if rietveld.private is set, and so we should ignore rietveld.cc only
1988 # when --private is specified explicitly on the command line.
1989 if options.private:
1990 logging.warn('rietveld.cc is ignored since private flag is specified. '
1991 'You need to review and add them manually if necessary.')
1992 cc = self.GetCCListWithoutDefault()
1993 else:
1994 cc = self.GetCCList()
1995 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1996 if cc:
1997 upload_args.extend(['--cc', cc])
1998
1999 if options.private or settings.GetDefaultPrivateFlag() == "True":
2000 upload_args.append('--private')
2001
2002 upload_args.extend(['--git_similarity', str(options.similarity)])
2003 if not options.find_copies:
2004 upload_args.extend(['--git_no_find_copies'])
2005
2006 # Include the upstream repo's URL in the change -- this is useful for
2007 # projects that have their source spread across multiple repos.
2008 remote_url = self.GetGitBaseUrlFromConfig()
2009 if not remote_url:
2010 if settings.GetIsGitSvn():
2011 remote_url = self.GetGitSvnRemoteUrl()
2012 else:
2013 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2014 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2015 self.GetUpstreamBranch().split('/')[-1])
2016 if remote_url:
2017 upload_args.extend(['--base_url', remote_url])
2018 remote, remote_branch = self.GetRemoteBranch()
2019 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2020 settings.GetPendingRefPrefix())
2021 if target_ref:
2022 upload_args.extend(['--target_ref', target_ref])
2023
2024 # Look for dependent patchsets. See crbug.com/480453 for more details.
2025 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2026 upstream_branch = ShortBranchName(upstream_branch)
2027 if remote is '.':
2028 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002029 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002030 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002031 print()
2032 print('Skipping dependency patchset upload because git config '
2033 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2034 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002035 else:
2036 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002037 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002038 auth_config=auth_config)
2039 branch_cl_issue_url = branch_cl.GetIssueURL()
2040 branch_cl_issue = branch_cl.GetIssue()
2041 branch_cl_patchset = branch_cl.GetPatchset()
2042 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2043 upload_args.extend(
2044 ['--depends_on_patchset', '%s:%s' % (
2045 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002046 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002047 '\n'
2048 'The current branch (%s) is tracking a local branch (%s) with '
2049 'an associated CL.\n'
2050 'Adding %s/#ps%s as a dependency patchset.\n'
2051 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2052 branch_cl_patchset))
2053
2054 project = settings.GetProject()
2055 if project:
2056 upload_args.extend(['--project', project])
2057
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002058 try:
2059 upload_args = ['upload'] + upload_args + args
2060 logging.info('upload.RealMain(%s)', upload_args)
2061 issue, patchset = upload.RealMain(upload_args)
2062 issue = int(issue)
2063 patchset = int(patchset)
2064 except KeyboardInterrupt:
2065 sys.exit(1)
2066 except:
2067 # If we got an exception after the user typed a description for their
2068 # change, back up the description before re-raising.
2069 if change_desc:
2070 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2071 print('\nGot exception while uploading -- saving description to %s\n' %
2072 backup_path)
2073 backup_file = open(backup_path, 'w')
2074 backup_file.write(change_desc.description)
2075 backup_file.close()
2076 raise
2077
2078 if not self.GetIssue():
2079 self.SetIssue(issue)
2080 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002081 return 0
2082
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002083
2084class _GerritChangelistImpl(_ChangelistCodereviewBase):
2085 def __init__(self, changelist, auth_config=None):
2086 # auth_config is Rietveld thing, kept here to preserve interface only.
2087 super(_GerritChangelistImpl, self).__init__(changelist)
2088 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002089 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002090 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002091 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002092
2093 def _GetGerritHost(self):
2094 # Lazy load of configs.
2095 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002096 if self._gerrit_host and '.' not in self._gerrit_host:
2097 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2098 # This happens for internal stuff http://crbug.com/614312.
2099 parsed = urlparse.urlparse(self.GetRemoteUrl())
2100 if parsed.scheme == 'sso':
2101 print('WARNING: using non https URLs for remote is likely broken\n'
2102 ' Your current remote is: %s' % self.GetRemoteUrl())
2103 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2104 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002105 return self._gerrit_host
2106
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002107 def _GetGitHost(self):
2108 """Returns git host to be used when uploading change to Gerrit."""
2109 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2110
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002111 def GetCodereviewServer(self):
2112 if not self._gerrit_server:
2113 # If we're on a branch then get the server potentially associated
2114 # with that branch.
2115 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002116 self._gerrit_server = self._GitGetBranchConfigValue(
2117 self.CodereviewServerConfigKey())
2118 if self._gerrit_server:
2119 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002120 if not self._gerrit_server:
2121 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2122 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002123 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002124 parts[0] = parts[0] + '-review'
2125 self._gerrit_host = '.'.join(parts)
2126 self._gerrit_server = 'https://%s' % self._gerrit_host
2127 return self._gerrit_server
2128
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002129 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002130 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002131 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002132
tandrii5d48c322016-08-18 16:19:37 -07002133 @classmethod
2134 def PatchsetConfigKey(cls):
2135 return 'gerritpatchset'
2136
2137 @classmethod
2138 def CodereviewServerConfigKey(cls):
2139 return 'gerritserver'
2140
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002141 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002142 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002143 if settings.GetGerritSkipEnsureAuthenticated():
2144 # For projects with unusual authentication schemes.
2145 # See http://crbug.com/603378.
2146 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002147 # Lazy-loader to identify Gerrit and Git hosts.
2148 if gerrit_util.GceAuthenticator.is_gce():
2149 return
2150 self.GetCodereviewServer()
2151 git_host = self._GetGitHost()
2152 assert self._gerrit_server and self._gerrit_host
2153 cookie_auth = gerrit_util.CookiesAuthenticator()
2154
2155 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2156 git_auth = cookie_auth.get_auth_header(git_host)
2157 if gerrit_auth and git_auth:
2158 if gerrit_auth == git_auth:
2159 return
2160 print((
2161 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2162 ' Check your %s or %s file for credentials of hosts:\n'
2163 ' %s\n'
2164 ' %s\n'
2165 ' %s') %
2166 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2167 git_host, self._gerrit_host,
2168 cookie_auth.get_new_password_message(git_host)))
2169 if not force:
2170 ask_for_data('If you know what you are doing, press Enter to continue, '
2171 'Ctrl+C to abort.')
2172 return
2173 else:
2174 missing = (
2175 [] if gerrit_auth else [self._gerrit_host] +
2176 [] if git_auth else [git_host])
2177 DieWithError('Credentials for the following hosts are required:\n'
2178 ' %s\n'
2179 'These are read from %s (or legacy %s)\n'
2180 '%s' % (
2181 '\n '.join(missing),
2182 cookie_auth.get_gitcookies_path(),
2183 cookie_auth.get_netrc_path(),
2184 cookie_auth.get_new_password_message(git_host)))
2185
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002186 def _PostUnsetIssueProperties(self):
2187 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002188 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002189
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002190 def GetRieveldObjForPresubmit(self):
2191 class ThisIsNotRietveldIssue(object):
2192 def __nonzero__(self):
2193 # This is a hack to make presubmit_support think that rietveld is not
2194 # defined, yet still ensure that calls directly result in a decent
2195 # exception message below.
2196 return False
2197
2198 def __getattr__(self, attr):
2199 print(
2200 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2201 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2202 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2203 'or use Rietveld for codereview.\n'
2204 'See also http://crbug.com/579160.' % attr)
2205 raise NotImplementedError()
2206 return ThisIsNotRietveldIssue()
2207
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002208 def GetGerritObjForPresubmit(self):
2209 return presubmit_support.GerritAccessor(self._GetGerritHost())
2210
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002211 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002212 """Apply a rough heuristic to give a simple summary of an issue's review
2213 or CQ status, assuming adherence to a common workflow.
2214
2215 Returns None if no issue for this branch, or one of the following keywords:
2216 * 'error' - error from review tool (including deleted issues)
2217 * 'unsent' - no reviewers added
2218 * 'waiting' - waiting for review
2219 * 'reply' - waiting for owner to reply to review
2220 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2221 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2222 * 'commit' - in the commit queue
2223 * 'closed' - abandoned
2224 """
2225 if not self.GetIssue():
2226 return None
2227
2228 try:
2229 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2230 except httplib.HTTPException:
2231 return 'error'
2232
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002233 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002234 return 'closed'
2235
2236 cq_label = data['labels'].get('Commit-Queue', {})
2237 if cq_label:
2238 # Vote value is a stringified integer, which we expect from 0 to 2.
2239 vote_value = cq_label.get('value', '0')
2240 vote_text = cq_label.get('values', {}).get(vote_value, '')
2241 if vote_text.lower() == 'commit':
2242 return 'commit'
2243
2244 lgtm_label = data['labels'].get('Code-Review', {})
2245 if lgtm_label:
2246 if 'rejected' in lgtm_label:
2247 return 'not lgtm'
2248 if 'approved' in lgtm_label:
2249 return 'lgtm'
2250
2251 if not data.get('reviewers', {}).get('REVIEWER', []):
2252 return 'unsent'
2253
2254 messages = data.get('messages', [])
2255 if messages:
2256 owner = data['owner'].get('_account_id')
2257 last_message_author = messages[-1].get('author', {}).get('_account_id')
2258 if owner != last_message_author:
2259 # Some reply from non-owner.
2260 return 'reply'
2261
2262 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002263
2264 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002265 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002266 return data['revisions'][data['current_revision']]['_number']
2267
2268 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002269 data = self._GetChangeDetail(['CURRENT_REVISION'])
2270 current_rev = data['current_revision']
2271 url = data['revisions'][current_rev]['fetch']['http']['url']
2272 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002273
2274 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002275 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2276 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002277
2278 def CloseIssue(self):
2279 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2280
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002281 def GetApprovingReviewers(self):
2282 """Returns a list of reviewers approving the change.
2283
2284 Note: not necessarily committers.
2285 """
2286 raise NotImplementedError()
2287
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002288 def SubmitIssue(self, wait_for_merge=True):
2289 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2290 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002291
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002292 def _GetChangeDetail(self, options=None, issue=None):
2293 options = options or []
2294 issue = issue or self.GetIssue()
2295 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002296 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2297 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002298
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002299 def CMDLand(self, force, bypass_hooks, verbose):
2300 if git_common.is_dirty_git_tree('land'):
2301 return 1
tandriid60367b2016-06-22 05:25:12 -07002302 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2303 if u'Commit-Queue' in detail.get('labels', {}):
2304 if not force:
2305 ask_for_data('\nIt seems this repository has a Commit Queue, '
2306 'which can test and land changes for you. '
2307 'Are you sure you wish to bypass it?\n'
2308 'Press Enter to continue, Ctrl+C to abort.')
2309
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002310 differs = True
tandrii5d48c322016-08-18 16:19:37 -07002311 last_upload = RunGit(['config', self._GitBranchSetting('gerritsquashhash')],
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002312 error_ok=True).strip()
2313 # Note: git diff outputs nothing if there is no diff.
2314 if not last_upload or RunGit(['diff', last_upload]).strip():
2315 print('WARNING: some changes from local branch haven\'t been uploaded')
2316 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002317 if detail['current_revision'] == last_upload:
2318 differs = False
2319 else:
2320 print('WARNING: local branch contents differ from latest uploaded '
2321 'patchset')
2322 if differs:
2323 if not force:
2324 ask_for_data(
2325 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2326 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2327 elif not bypass_hooks:
2328 hook_results = self.RunHook(
2329 committing=True,
2330 may_prompt=not force,
2331 verbose=verbose,
2332 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2333 if not hook_results.should_continue():
2334 return 1
2335
2336 self.SubmitIssue(wait_for_merge=True)
2337 print('Issue %s has been submitted.' % self.GetIssueURL())
2338 return 0
2339
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002340 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2341 directory):
2342 assert not reject
2343 assert not nocommit
2344 assert not directory
2345 assert parsed_issue_arg.valid
2346
2347 self._changelist.issue = parsed_issue_arg.issue
2348
2349 if parsed_issue_arg.hostname:
2350 self._gerrit_host = parsed_issue_arg.hostname
2351 self._gerrit_server = 'https://%s' % self._gerrit_host
2352
2353 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2354
2355 if not parsed_issue_arg.patchset:
2356 # Use current revision by default.
2357 revision_info = detail['revisions'][detail['current_revision']]
2358 patchset = int(revision_info['_number'])
2359 else:
2360 patchset = parsed_issue_arg.patchset
2361 for revision_info in detail['revisions'].itervalues():
2362 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2363 break
2364 else:
2365 DieWithError('Couldn\'t find patchset %i in issue %i' %
2366 (parsed_issue_arg.patchset, self.GetIssue()))
2367
2368 fetch_info = revision_info['fetch']['http']
2369 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2370 RunGit(['cherry-pick', 'FETCH_HEAD'])
2371 self.SetIssue(self.GetIssue())
2372 self.SetPatchset(patchset)
2373 print('Committed patch for issue %i pathset %i locally' %
2374 (self.GetIssue(), self.GetPatchset()))
2375 return 0
2376
2377 @staticmethod
2378 def ParseIssueURL(parsed_url):
2379 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2380 return None
2381 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2382 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2383 # Short urls like https://domain/<issue_number> can be used, but don't allow
2384 # specifying the patchset (you'd 404), but we allow that here.
2385 if parsed_url.path == '/':
2386 part = parsed_url.fragment
2387 else:
2388 part = parsed_url.path
2389 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2390 if match:
2391 return _ParsedIssueNumberArgument(
2392 issue=int(match.group(2)),
2393 patchset=int(match.group(4)) if match.group(4) else None,
2394 hostname=parsed_url.netloc)
2395 return None
2396
tandrii16e0b4e2016-06-07 10:34:28 -07002397 def _GerritCommitMsgHookCheck(self, offer_removal):
2398 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2399 if not os.path.exists(hook):
2400 return
2401 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2402 # custom developer made one.
2403 data = gclient_utils.FileRead(hook)
2404 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2405 return
2406 print('Warning: you have Gerrit commit-msg hook installed.\n'
2407 'It is not neccessary for uploading with git cl in squash mode, '
2408 'and may interfere with it in subtle ways.\n'
2409 'We recommend you remove the commit-msg hook.')
2410 if offer_removal:
2411 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2412 if reply.lower().startswith('y'):
2413 gclient_utils.rm_file_or_tree(hook)
2414 print('Gerrit commit-msg hook removed.')
2415 else:
2416 print('OK, will keep Gerrit commit-msg hook in place.')
2417
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002418 def CMDUploadChange(self, options, args, change):
2419 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002420 if options.squash and options.no_squash:
2421 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002422
2423 if not options.squash and not options.no_squash:
2424 # Load default for user, repo, squash=true, in this order.
2425 options.squash = settings.GetSquashGerritUploads()
2426 elif options.no_squash:
2427 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002428
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002429 # We assume the remote called "origin" is the one we want.
2430 # It is probably not worthwhile to support different workflows.
2431 gerrit_remote = 'origin'
2432
2433 remote, remote_branch = self.GetRemoteBranch()
2434 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2435 pending_prefix='')
2436
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002437 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002438 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002439 if self.GetIssue():
2440 # Try to get the message from a previous upload.
2441 message = self.GetDescription()
2442 if not message:
2443 DieWithError(
2444 'failed to fetch description from current Gerrit issue %d\n'
2445 '%s' % (self.GetIssue(), self.GetIssueURL()))
2446 change_id = self._GetChangeDetail()['change_id']
2447 while True:
2448 footer_change_ids = git_footers.get_footer_change_id(message)
2449 if footer_change_ids == [change_id]:
2450 break
2451 if not footer_change_ids:
2452 message = git_footers.add_footer_change_id(message, change_id)
2453 print('WARNING: appended missing Change-Id to issue description')
2454 continue
2455 # There is already a valid footer but with different or several ids.
2456 # Doing this automatically is non-trivial as we don't want to lose
2457 # existing other footers, yet we want to append just 1 desired
2458 # Change-Id. Thus, just create a new footer, but let user verify the
2459 # new description.
2460 message = '%s\n\nChange-Id: %s' % (message, change_id)
2461 print(
2462 'WARNING: issue %s has Change-Id footer(s):\n'
2463 ' %s\n'
2464 'but issue has Change-Id %s, according to Gerrit.\n'
2465 'Please, check the proposed correction to the description, '
2466 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2467 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2468 change_id))
2469 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2470 if not options.force:
2471 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002472 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002473 message = change_desc.description
2474 if not message:
2475 DieWithError("Description is empty. Aborting...")
2476 # Continue the while loop.
2477 # Sanity check of this code - we should end up with proper message
2478 # footer.
2479 assert [change_id] == git_footers.get_footer_change_id(message)
2480 change_desc = ChangeDescription(message)
2481 else:
2482 change_desc = ChangeDescription(
2483 options.message or CreateDescriptionFromLog(args))
2484 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002485 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002486 if not change_desc.description:
2487 DieWithError("Description is empty. Aborting...")
2488 message = change_desc.description
2489 change_ids = git_footers.get_footer_change_id(message)
2490 if len(change_ids) > 1:
2491 DieWithError('too many Change-Id footers, at most 1 allowed.')
2492 if not change_ids:
2493 # Generate the Change-Id automatically.
2494 message = git_footers.add_footer_change_id(
2495 message, GenerateGerritChangeId(message))
2496 change_desc.set_description(message)
2497 change_ids = git_footers.get_footer_change_id(message)
2498 assert len(change_ids) == 1
2499 change_id = change_ids[0]
2500
2501 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2502 if remote is '.':
2503 # If our upstream branch is local, we base our squashed commit on its
2504 # squashed version.
2505 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2506 # Check the squashed hash of the parent.
2507 parent = RunGit(['config',
2508 'branch.%s.gerritsquashhash' % upstream_branch_name],
2509 error_ok=True).strip()
2510 # Verify that the upstream branch has been uploaded too, otherwise
2511 # Gerrit will create additional CLs when uploading.
2512 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2513 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002514 DieWithError(
2515 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002516 'Note: maybe you\'ve uploaded it with --no-squash. '
2517 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002518 ' git cl upload --squash\n' % upstream_branch_name)
2519 else:
2520 parent = self.GetCommonAncestorWithUpstream()
2521
2522 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2523 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2524 '-m', message]).strip()
2525 else:
2526 change_desc = ChangeDescription(
2527 options.message or CreateDescriptionFromLog(args))
2528 if not change_desc.description:
2529 DieWithError("Description is empty. Aborting...")
2530
2531 if not git_footers.get_footer_change_id(change_desc.description):
2532 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002533 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2534 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002535 ref_to_push = 'HEAD'
2536 parent = '%s/%s' % (gerrit_remote, branch)
2537 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2538
2539 assert change_desc
2540 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2541 ref_to_push)]).splitlines()
2542 if len(commits) > 1:
2543 print('WARNING: This will upload %d commits. Run the following command '
2544 'to see which commits will be uploaded: ' % len(commits))
2545 print('git log %s..%s' % (parent, ref_to_push))
2546 print('You can also use `git squash-branch` to squash these into a '
2547 'single commit.')
2548 ask_for_data('About to upload; enter to confirm.')
2549
2550 if options.reviewers or options.tbr_owners:
2551 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2552 change)
2553
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002554 # Extra options that can be specified at push time. Doc:
2555 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2556 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002557 if change_desc.get_reviewers(tbr_only=True):
2558 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2559 refspec_opts.append('l=Code-Review+1')
2560
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002561 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002562 if not re.match(r'^[\w ]+$', options.title):
2563 options.title = re.sub(r'[^\w ]', '', options.title)
2564 print('WARNING: Patchset title may only contain alphanumeric chars '
2565 'and spaces. Cleaned up title:\n%s' % options.title)
2566 if not options.force:
2567 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002568 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2569 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002570 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2571
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002572 if options.send_mail:
2573 if not change_desc.get_reviewers():
2574 DieWithError('Must specify reviewers to send email.')
2575 refspec_opts.append('notify=ALL')
2576 else:
2577 refspec_opts.append('notify=NONE')
2578
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002579 cc = self.GetCCList().split(',')
2580 if options.cc:
2581 cc.extend(options.cc)
2582 cc = filter(None, cc)
2583 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002584 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002585
tandrii99a72f22016-08-17 14:33:24 -07002586 reviewers = change_desc.get_reviewers()
2587 if reviewers:
2588 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002589
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002590 refspec_suffix = ''
2591 if refspec_opts:
2592 refspec_suffix = '%' + ','.join(refspec_opts)
2593 assert ' ' not in refspec_suffix, (
2594 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002595 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002596
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002597 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002598 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002599 print_stdout=True,
2600 # Flush after every line: useful for seeing progress when running as
2601 # recipe.
2602 filter_fn=lambda _: sys.stdout.flush())
2603
2604 if options.squash:
2605 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2606 change_numbers = [m.group(1)
2607 for m in map(regex.match, push_stdout.splitlines())
2608 if m]
2609 if len(change_numbers) != 1:
2610 DieWithError(
2611 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2612 'Change-Id: %s') % (len(change_numbers), change_id))
2613 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002614 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002615 return 0
2616
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002617 def _AddChangeIdToCommitMessage(self, options, args):
2618 """Re-commits using the current message, assumes the commit hook is in
2619 place.
2620 """
2621 log_desc = options.message or CreateDescriptionFromLog(args)
2622 git_command = ['commit', '--amend', '-m', log_desc]
2623 RunGit(git_command)
2624 new_log_desc = CreateDescriptionFromLog(args)
2625 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002626 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002627 return new_log_desc
2628 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002629 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002630
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002631 def SetCQState(self, new_state):
2632 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002633 vote_map = {
2634 _CQState.NONE: 0,
2635 _CQState.DRY_RUN: 1,
2636 _CQState.COMMIT : 2,
2637 }
2638 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2639 labels={'Commit-Queue': vote_map[new_state]})
2640
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002641
2642_CODEREVIEW_IMPLEMENTATIONS = {
2643 'rietveld': _RietveldChangelistImpl,
2644 'gerrit': _GerritChangelistImpl,
2645}
2646
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002647
iannuccie53c9352016-08-17 14:40:40 -07002648def _add_codereview_issue_select_options(parser, extra=""):
2649 _add_codereview_select_options(parser)
2650
2651 text = ('Operate on this issue number instead of the current branch\'s '
2652 'implicit issue.')
2653 if extra:
2654 text += ' '+extra
2655 parser.add_option('-i', '--issue', type=int, help=text)
2656
2657
2658def _process_codereview_issue_select_options(parser, options):
2659 _process_codereview_select_options(parser, options)
2660 if options.issue is not None and not options.forced_codereview:
2661 parser.error('--issue must be specified with either --rietveld or --gerrit')
2662
2663
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002664def _add_codereview_select_options(parser):
2665 """Appends --gerrit and --rietveld options to force specific codereview."""
2666 parser.codereview_group = optparse.OptionGroup(
2667 parser, 'EXPERIMENTAL! Codereview override options')
2668 parser.add_option_group(parser.codereview_group)
2669 parser.codereview_group.add_option(
2670 '--gerrit', action='store_true',
2671 help='Force the use of Gerrit for codereview')
2672 parser.codereview_group.add_option(
2673 '--rietveld', action='store_true',
2674 help='Force the use of Rietveld for codereview')
2675
2676
2677def _process_codereview_select_options(parser, options):
2678 if options.gerrit and options.rietveld:
2679 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2680 options.forced_codereview = None
2681 if options.gerrit:
2682 options.forced_codereview = 'gerrit'
2683 elif options.rietveld:
2684 options.forced_codereview = 'rietveld'
2685
2686
tandriif9aefb72016-07-01 09:06:51 -07002687def _get_bug_line_values(default_project, bugs):
2688 """Given default_project and comma separated list of bugs, yields bug line
2689 values.
2690
2691 Each bug can be either:
2692 * a number, which is combined with default_project
2693 * string, which is left as is.
2694
2695 This function may produce more than one line, because bugdroid expects one
2696 project per line.
2697
2698 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2699 ['v8:123', 'chromium:789']
2700 """
2701 default_bugs = []
2702 others = []
2703 for bug in bugs.split(','):
2704 bug = bug.strip()
2705 if bug:
2706 try:
2707 default_bugs.append(int(bug))
2708 except ValueError:
2709 others.append(bug)
2710
2711 if default_bugs:
2712 default_bugs = ','.join(map(str, default_bugs))
2713 if default_project:
2714 yield '%s:%s' % (default_project, default_bugs)
2715 else:
2716 yield default_bugs
2717 for other in sorted(others):
2718 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2719 yield other
2720
2721
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002722class ChangeDescription(object):
2723 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002724 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002725 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002726
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002727 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002728 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002729
agable@chromium.org42c20792013-09-12 17:34:49 +00002730 @property # www.logilab.org/ticket/89786
2731 def description(self): # pylint: disable=E0202
2732 return '\n'.join(self._description_lines)
2733
2734 def set_description(self, desc):
2735 if isinstance(desc, basestring):
2736 lines = desc.splitlines()
2737 else:
2738 lines = [line.rstrip() for line in desc]
2739 while lines and not lines[0]:
2740 lines.pop(0)
2741 while lines and not lines[-1]:
2742 lines.pop(-1)
2743 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002744
piman@chromium.org336f9122014-09-04 02:16:55 +00002745 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002746 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002747 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002748 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002749 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002750 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002751
agable@chromium.org42c20792013-09-12 17:34:49 +00002752 # Get the set of R= and TBR= lines and remove them from the desciption.
2753 regexp = re.compile(self.R_LINE)
2754 matches = [regexp.match(line) for line in self._description_lines]
2755 new_desc = [l for i, l in enumerate(self._description_lines)
2756 if not matches[i]]
2757 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002758
agable@chromium.org42c20792013-09-12 17:34:49 +00002759 # Construct new unified R= and TBR= lines.
2760 r_names = []
2761 tbr_names = []
2762 for match in matches:
2763 if not match:
2764 continue
2765 people = cleanup_list([match.group(2).strip()])
2766 if match.group(1) == 'TBR':
2767 tbr_names.extend(people)
2768 else:
2769 r_names.extend(people)
2770 for name in r_names:
2771 if name not in reviewers:
2772 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002773 if add_owners_tbr:
2774 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002775 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002776 all_reviewers = set(tbr_names + reviewers)
2777 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2778 all_reviewers)
2779 tbr_names.extend(owners_db.reviewers_for(missing_files,
2780 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002781 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2782 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2783
2784 # Put the new lines in the description where the old first R= line was.
2785 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2786 if 0 <= line_loc < len(self._description_lines):
2787 if new_tbr_line:
2788 self._description_lines.insert(line_loc, new_tbr_line)
2789 if new_r_line:
2790 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002791 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002792 if new_r_line:
2793 self.append_footer(new_r_line)
2794 if new_tbr_line:
2795 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002796
tandriif9aefb72016-07-01 09:06:51 -07002797 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002798 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002799 self.set_description([
2800 '# Enter a description of the change.',
2801 '# This will be displayed on the codereview site.',
2802 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002803 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002804 '--------------------',
2805 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002806
agable@chromium.org42c20792013-09-12 17:34:49 +00002807 regexp = re.compile(self.BUG_LINE)
2808 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002809 prefix = settings.GetBugPrefix()
2810 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2811 for value in values:
2812 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2813 self.append_footer('BUG=%s' % value)
2814
agable@chromium.org42c20792013-09-12 17:34:49 +00002815 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002816 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002817 if not content:
2818 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002819 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002820
2821 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002822 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2823 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002824 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002825 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002826
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002827 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002828 """Adds a footer line to the description.
2829
2830 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2831 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2832 that Gerrit footers are always at the end.
2833 """
2834 parsed_footer_line = git_footers.parse_footer(line)
2835 if parsed_footer_line:
2836 # Line is a gerrit footer in the form: Footer-Key: any value.
2837 # Thus, must be appended observing Gerrit footer rules.
2838 self.set_description(
2839 git_footers.add_footer(self.description,
2840 key=parsed_footer_line[0],
2841 value=parsed_footer_line[1]))
2842 return
2843
2844 if not self._description_lines:
2845 self._description_lines.append(line)
2846 return
2847
2848 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2849 if gerrit_footers:
2850 # git_footers.split_footers ensures that there is an empty line before
2851 # actual (gerrit) footers, if any. We have to keep it that way.
2852 assert top_lines and top_lines[-1] == ''
2853 top_lines, separator = top_lines[:-1], top_lines[-1:]
2854 else:
2855 separator = [] # No need for separator if there are no gerrit_footers.
2856
2857 prev_line = top_lines[-1] if top_lines else ''
2858 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2859 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2860 top_lines.append('')
2861 top_lines.append(line)
2862 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002863
tandrii99a72f22016-08-17 14:33:24 -07002864 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002865 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002866 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002867 reviewers = [match.group(2).strip()
2868 for match in matches
2869 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002870 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002871
2872
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002873def get_approving_reviewers(props):
2874 """Retrieves the reviewers that approved a CL from the issue properties with
2875 messages.
2876
2877 Note that the list may contain reviewers that are not committer, thus are not
2878 considered by the CQ.
2879 """
2880 return sorted(
2881 set(
2882 message['sender']
2883 for message in props['messages']
2884 if message['approval'] and message['sender'] in props['reviewers']
2885 )
2886 )
2887
2888
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002889def FindCodereviewSettingsFile(filename='codereview.settings'):
2890 """Finds the given file starting in the cwd and going up.
2891
2892 Only looks up to the top of the repository unless an
2893 'inherit-review-settings-ok' file exists in the root of the repository.
2894 """
2895 inherit_ok_file = 'inherit-review-settings-ok'
2896 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002897 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002898 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2899 root = '/'
2900 while True:
2901 if filename in os.listdir(cwd):
2902 if os.path.isfile(os.path.join(cwd, filename)):
2903 return open(os.path.join(cwd, filename))
2904 if cwd == root:
2905 break
2906 cwd = os.path.dirname(cwd)
2907
2908
2909def LoadCodereviewSettingsFromFile(fileobj):
2910 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002911 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002913 def SetProperty(name, setting, unset_error_ok=False):
2914 fullname = 'rietveld.' + name
2915 if setting in keyvals:
2916 RunGit(['config', fullname, keyvals[setting]])
2917 else:
2918 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2919
2920 SetProperty('server', 'CODE_REVIEW_SERVER')
2921 # Only server setting is required. Other settings can be absent.
2922 # In that case, we ignore errors raised during option deletion attempt.
2923 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002924 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002925 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2926 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002927 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002928 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002929 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2930 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002931 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002932 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002933 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002934 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2935 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002936
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002937 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002938 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002939
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002940 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002941 RunGit(['config', 'gerrit.squash-uploads',
2942 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002943
tandrii@chromium.org28253532016-04-14 13:46:56 +00002944 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002945 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002946 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2947
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002948 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2949 #should be of the form
2950 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2951 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2952 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2953 keyvals['ORIGIN_URL_CONFIG']])
2954
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002955
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002956def urlretrieve(source, destination):
2957 """urllib is broken for SSL connections via a proxy therefore we
2958 can't use urllib.urlretrieve()."""
2959 with open(destination, 'w') as f:
2960 f.write(urllib2.urlopen(source).read())
2961
2962
ukai@chromium.org712d6102013-11-27 00:52:58 +00002963def hasSheBang(fname):
2964 """Checks fname is a #! script."""
2965 with open(fname) as f:
2966 return f.read(2).startswith('#!')
2967
2968
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002969# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2970def DownloadHooks(*args, **kwargs):
2971 pass
2972
2973
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002974def DownloadGerritHook(force):
2975 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002976
2977 Args:
2978 force: True to update hooks. False to install hooks if not present.
2979 """
2980 if not settings.GetIsGerrit():
2981 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002982 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002983 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2984 if not os.access(dst, os.X_OK):
2985 if os.path.exists(dst):
2986 if not force:
2987 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002988 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002989 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002990 if not hasSheBang(dst):
2991 DieWithError('Not a script: %s\n'
2992 'You need to download from\n%s\n'
2993 'into .git/hooks/commit-msg and '
2994 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002995 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2996 except Exception:
2997 if os.path.exists(dst):
2998 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002999 DieWithError('\nFailed to download hooks.\n'
3000 'You need to download from\n%s\n'
3001 'into .git/hooks/commit-msg and '
3002 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003003
3004
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003005
3006def GetRietveldCodereviewSettingsInteractively():
3007 """Prompt the user for settings."""
3008 server = settings.GetDefaultServerUrl(error_ok=True)
3009 prompt = 'Rietveld server (host[:port])'
3010 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3011 newserver = ask_for_data(prompt + ':')
3012 if not server and not newserver:
3013 newserver = DEFAULT_SERVER
3014 if newserver:
3015 newserver = gclient_utils.UpgradeToHttps(newserver)
3016 if newserver != server:
3017 RunGit(['config', 'rietveld.server', newserver])
3018
3019 def SetProperty(initial, caption, name, is_url):
3020 prompt = caption
3021 if initial:
3022 prompt += ' ("x" to clear) [%s]' % initial
3023 new_val = ask_for_data(prompt + ':')
3024 if new_val == 'x':
3025 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3026 elif new_val:
3027 if is_url:
3028 new_val = gclient_utils.UpgradeToHttps(new_val)
3029 if new_val != initial:
3030 RunGit(['config', 'rietveld.' + name, new_val])
3031
3032 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3033 SetProperty(settings.GetDefaultPrivateFlag(),
3034 'Private flag (rietveld only)', 'private', False)
3035 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3036 'tree-status-url', False)
3037 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3038 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3039 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3040 'run-post-upload-hook', False)
3041
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003042@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003043def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003044 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003045
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003046 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003047 'For Gerrit, see http://crbug.com/603116.')
3048 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003049 parser.add_option('--activate-update', action='store_true',
3050 help='activate auto-updating [rietveld] section in '
3051 '.git/config')
3052 parser.add_option('--deactivate-update', action='store_true',
3053 help='deactivate auto-updating [rietveld] section in '
3054 '.git/config')
3055 options, args = parser.parse_args(args)
3056
3057 if options.deactivate_update:
3058 RunGit(['config', 'rietveld.autoupdate', 'false'])
3059 return
3060
3061 if options.activate_update:
3062 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3063 return
3064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003065 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003066 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003067 return 0
3068
3069 url = args[0]
3070 if not url.endswith('codereview.settings'):
3071 url = os.path.join(url, 'codereview.settings')
3072
3073 # Load code review settings and download hooks (if available).
3074 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3075 return 0
3076
3077
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003078def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003079 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003080 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3081 branch = ShortBranchName(branchref)
3082 _, args = parser.parse_args(args)
3083 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003084 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003085 return RunGit(['config', 'branch.%s.base-url' % branch],
3086 error_ok=False).strip()
3087 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003088 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003089 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3090 error_ok=False).strip()
3091
3092
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003093def color_for_status(status):
3094 """Maps a Changelist status to color, for CMDstatus and other tools."""
3095 return {
3096 'unsent': Fore.RED,
3097 'waiting': Fore.BLUE,
3098 'reply': Fore.YELLOW,
3099 'lgtm': Fore.GREEN,
3100 'commit': Fore.MAGENTA,
3101 'closed': Fore.CYAN,
3102 'error': Fore.WHITE,
3103 }.get(status, Fore.WHITE)
3104
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003105
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003106def get_cl_statuses(changes, fine_grained, max_processes=None):
3107 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003108
3109 If fine_grained is true, this will fetch CL statuses from the server.
3110 Otherwise, simply indicate if there's a matching url for the given branches.
3111
3112 If max_processes is specified, it is used as the maximum number of processes
3113 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3114 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003115
3116 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003117 """
3118 # Silence upload.py otherwise it becomes unwieldly.
3119 upload.verbosity = 0
3120
3121 if fine_grained:
3122 # Process one branch synchronously to work through authentication, then
3123 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003124 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003125 def fetch(cl):
3126 try:
3127 return (cl, cl.GetStatus())
3128 except:
3129 # See http://crbug.com/629863.
3130 logging.exception('failed to fetch status for %s:', cl)
3131 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003132 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003133
tandriiea9514a2016-08-17 12:32:37 -07003134 changes_to_fetch = changes[1:]
3135 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003136 # Exit early if there was only one branch to fetch.
3137 return
3138
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003139 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003140 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003141 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003142 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003143
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003144 fetched_cls = set()
3145 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003146 while True:
3147 try:
3148 row = it.next(timeout=5)
3149 except multiprocessing.TimeoutError:
3150 break
3151
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003152 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003153 yield row
3154
3155 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003156 for cl in set(changes_to_fetch) - fetched_cls:
3157 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003158
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003159 else:
3160 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003161 for cl in changes:
3162 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003163
rmistry@google.com2dd99862015-06-22 12:22:18 +00003164
3165def upload_branch_deps(cl, args):
3166 """Uploads CLs of local branches that are dependents of the current branch.
3167
3168 If the local branch dependency tree looks like:
3169 test1 -> test2.1 -> test3.1
3170 -> test3.2
3171 -> test2.2 -> test3.3
3172
3173 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3174 run on the dependent branches in this order:
3175 test2.1, test3.1, test3.2, test2.2, test3.3
3176
3177 Note: This function does not rebase your local dependent branches. Use it when
3178 you make a change to the parent branch that will not conflict with its
3179 dependent branches, and you would like their dependencies updated in
3180 Rietveld.
3181 """
3182 if git_common.is_dirty_git_tree('upload-branch-deps'):
3183 return 1
3184
3185 root_branch = cl.GetBranch()
3186 if root_branch is None:
3187 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3188 'Get on a branch!')
3189 if not cl.GetIssue() or not cl.GetPatchset():
3190 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3191 'patchset dependencies without an uploaded CL.')
3192
3193 branches = RunGit(['for-each-ref',
3194 '--format=%(refname:short) %(upstream:short)',
3195 'refs/heads'])
3196 if not branches:
3197 print('No local branches found.')
3198 return 0
3199
3200 # Create a dictionary of all local branches to the branches that are dependent
3201 # on it.
3202 tracked_to_dependents = collections.defaultdict(list)
3203 for b in branches.splitlines():
3204 tokens = b.split()
3205 if len(tokens) == 2:
3206 branch_name, tracked = tokens
3207 tracked_to_dependents[tracked].append(branch_name)
3208
vapiera7fbd5a2016-06-16 09:17:49 -07003209 print()
3210 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003211 dependents = []
3212 def traverse_dependents_preorder(branch, padding=''):
3213 dependents_to_process = tracked_to_dependents.get(branch, [])
3214 padding += ' '
3215 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003216 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003217 dependents.append(dependent)
3218 traverse_dependents_preorder(dependent, padding)
3219 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003220 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003221
3222 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003223 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003224 return 0
3225
vapiera7fbd5a2016-06-16 09:17:49 -07003226 print('This command will checkout all dependent branches and run '
3227 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003228 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3229
andybons@chromium.org962f9462016-02-03 20:00:42 +00003230 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003231 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003232 args.extend(['-t', 'Updated patchset dependency'])
3233
rmistry@google.com2dd99862015-06-22 12:22:18 +00003234 # Record all dependents that failed to upload.
3235 failures = {}
3236 # Go through all dependents, checkout the branch and upload.
3237 try:
3238 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003239 print()
3240 print('--------------------------------------')
3241 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003242 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003243 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003244 try:
3245 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003246 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003247 failures[dependent_branch] = 1
3248 except: # pylint: disable=W0702
3249 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003250 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003251 finally:
3252 # Swap back to the original root branch.
3253 RunGit(['checkout', '-q', root_branch])
3254
vapiera7fbd5a2016-06-16 09:17:49 -07003255 print()
3256 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003257 for dependent_branch in dependents:
3258 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003259 print(' %s : %s' % (dependent_branch, upload_status))
3260 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003261
3262 return 0
3263
3264
kmarshall3bff56b2016-06-06 18:31:47 -07003265def CMDarchive(parser, args):
3266 """Archives and deletes branches associated with closed changelists."""
3267 parser.add_option(
3268 '-j', '--maxjobs', action='store', type=int,
3269 help='The maximum number of jobs to use when retrieving review status')
3270 parser.add_option(
3271 '-f', '--force', action='store_true',
3272 help='Bypasses the confirmation prompt.')
3273
3274 auth.add_auth_options(parser)
3275 options, args = parser.parse_args(args)
3276 if args:
3277 parser.error('Unsupported args: %s' % ' '.join(args))
3278 auth_config = auth.extract_auth_config_from_options(options)
3279
3280 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3281 if not branches:
3282 return 0
3283
vapiera7fbd5a2016-06-16 09:17:49 -07003284 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003285 changes = [Changelist(branchref=b, auth_config=auth_config)
3286 for b in branches.splitlines()]
3287 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3288 statuses = get_cl_statuses(changes,
3289 fine_grained=True,
3290 max_processes=options.maxjobs)
3291 proposal = [(cl.GetBranch(),
3292 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3293 for cl, status in statuses
3294 if status == 'closed']
3295 proposal.sort()
3296
3297 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003298 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003299 return 0
3300
3301 current_branch = GetCurrentBranch()
3302
vapiera7fbd5a2016-06-16 09:17:49 -07003303 print('\nBranches with closed issues that will be archived:\n')
3304 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003305 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003306 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003307
3308 if any(branch == current_branch for branch, _ in proposal):
3309 print('You are currently on a branch \'%s\' which is associated with a '
3310 'closed codereview issue, so archive cannot proceed. Please '
3311 'checkout another branch and run this command again.' %
3312 current_branch)
3313 return 1
3314
3315 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003316 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3317 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003318 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003319 return 1
3320
3321 for branch, tagname in proposal:
3322 RunGit(['tag', tagname, branch])
3323 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003324 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003325
3326 return 0
3327
3328
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003329def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003330 """Show status of changelists.
3331
3332 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003333 - Red not sent for review or broken
3334 - Blue waiting for review
3335 - Yellow waiting for you to reply to review
3336 - Green LGTM'ed
3337 - Magenta in the commit queue
3338 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003339
3340 Also see 'git cl comments'.
3341 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003342 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003343 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003344 parser.add_option('-f', '--fast', action='store_true',
3345 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003346 parser.add_option(
3347 '-j', '--maxjobs', action='store', type=int,
3348 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003349
3350 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003351 _add_codereview_issue_select_options(
3352 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003353 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003354 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003355 if args:
3356 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003357 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003358
iannuccie53c9352016-08-17 14:40:40 -07003359 if options.issue is not None and not options.field:
3360 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003363 cl = Changelist(auth_config=auth_config, issue=options.issue,
3364 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003365 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003366 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003367 elif options.field == 'id':
3368 issueid = cl.GetIssue()
3369 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003370 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003371 elif options.field == 'patch':
3372 patchset = cl.GetPatchset()
3373 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003374 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003375 elif options.field == 'status':
3376 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003377 elif options.field == 'url':
3378 url = cl.GetIssueURL()
3379 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003380 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003381 return 0
3382
3383 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3384 if not branches:
3385 print('No local branch found.')
3386 return 0
3387
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003388 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003389 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003390 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003391 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003392 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003393 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003394 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003395
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003396 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003397 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3398 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3399 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003400 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003401 c, status = output.next()
3402 branch_statuses[c.GetBranch()] = status
3403 status = branch_statuses.pop(branch)
3404 url = cl.GetIssueURL()
3405 if url and (not status or status == 'error'):
3406 # The issue probably doesn't exist anymore.
3407 url += ' (broken)'
3408
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003409 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003410 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003411 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003412 color = ''
3413 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003414 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003415 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003416 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003417 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003418
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003419 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003420 print()
3421 print('Current branch:',)
3422 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003423 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003424 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003425 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003426 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003427 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003428 print('Issue description:')
3429 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003430 return 0
3431
3432
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003433def colorize_CMDstatus_doc():
3434 """To be called once in main() to add colors to git cl status help."""
3435 colors = [i for i in dir(Fore) if i[0].isupper()]
3436
3437 def colorize_line(line):
3438 for color in colors:
3439 if color in line.upper():
3440 # Extract whitespaces first and the leading '-'.
3441 indent = len(line) - len(line.lstrip(' ')) + 1
3442 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3443 return line
3444
3445 lines = CMDstatus.__doc__.splitlines()
3446 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3447
3448
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003449@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003450def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003451 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003452
3453 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003454 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003455 parser.add_option('-r', '--reverse', action='store_true',
3456 help='Lookup the branch(es) for the specified issues. If '
3457 'no issues are specified, all branches with mapped '
3458 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003459 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003460 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003461 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003462
dnj@chromium.org406c4402015-03-03 17:22:28 +00003463 if options.reverse:
3464 branches = RunGit(['for-each-ref', 'refs/heads',
3465 '--format=%(refname:short)']).splitlines()
3466
3467 # Reverse issue lookup.
3468 issue_branch_map = {}
3469 for branch in branches:
3470 cl = Changelist(branchref=branch)
3471 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3472 if not args:
3473 args = sorted(issue_branch_map.iterkeys())
3474 for issue in args:
3475 if not issue:
3476 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003477 print('Branch for issue number %s: %s' % (
3478 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003479 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003480 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003481 if len(args) > 0:
3482 try:
3483 issue = int(args[0])
3484 except ValueError:
3485 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003486 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003487 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003488 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003489 return 0
3490
3491
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003492def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003493 """Shows or posts review comments for any changelist."""
3494 parser.add_option('-a', '--add-comment', dest='comment',
3495 help='comment to add to an issue')
3496 parser.add_option('-i', dest='issue',
3497 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003498 parser.add_option('-j', '--json-file',
3499 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003500 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003501 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003502 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003503
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003504 issue = None
3505 if options.issue:
3506 try:
3507 issue = int(options.issue)
3508 except ValueError:
3509 DieWithError('A review issue id is expected to be a number')
3510
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003511 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003512
3513 if options.comment:
3514 cl.AddComment(options.comment)
3515 return 0
3516
3517 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003518 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003519 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003520 summary.append({
3521 'date': message['date'],
3522 'lgtm': False,
3523 'message': message['text'],
3524 'not_lgtm': False,
3525 'sender': message['sender'],
3526 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003527 if message['disapproval']:
3528 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003529 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003530 elif message['approval']:
3531 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003532 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003533 elif message['sender'] == data['owner_email']:
3534 color = Fore.MAGENTA
3535 else:
3536 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003537 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003538 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003539 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003540 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003541 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003542 if options.json_file:
3543 with open(options.json_file, 'wb') as f:
3544 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003545 return 0
3546
3547
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003548@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003549def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003550 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003551 parser.add_option('-d', '--display', action='store_true',
3552 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003553 parser.add_option('-n', '--new-description',
3554 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003555
3556 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003557 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003558 options, args = parser.parse_args(args)
3559 _process_codereview_select_options(parser, options)
3560
3561 target_issue = None
3562 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003563 target_issue = ParseIssueNumberArgument(args[0])
3564 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003565 parser.print_help()
3566 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003567
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003568 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003569
martiniss6eda05f2016-06-30 10:18:35 -07003570 kwargs = {
3571 'auth_config': auth_config,
3572 'codereview': options.forced_codereview,
3573 }
3574 if target_issue:
3575 kwargs['issue'] = target_issue.issue
3576 if options.forced_codereview == 'rietveld':
3577 kwargs['rietveld_server'] = target_issue.hostname
3578
3579 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003580
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003581 if not cl.GetIssue():
3582 DieWithError('This branch has no associated changelist.')
3583 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003584
smut@google.com34fb6b12015-07-13 20:03:26 +00003585 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003586 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003587 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003588
3589 if options.new_description:
3590 text = options.new_description
3591 if text == '-':
3592 text = '\n'.join(l.rstrip() for l in sys.stdin)
3593
3594 description.set_description(text)
3595 else:
3596 description.prompt()
3597
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003598 if cl.GetDescription() != description.description:
3599 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003600 return 0
3601
3602
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003603def CreateDescriptionFromLog(args):
3604 """Pulls out the commit log to use as a base for the CL description."""
3605 log_args = []
3606 if len(args) == 1 and not args[0].endswith('.'):
3607 log_args = [args[0] + '..']
3608 elif len(args) == 1 and args[0].endswith('...'):
3609 log_args = [args[0][:-1]]
3610 elif len(args) == 2:
3611 log_args = [args[0] + '..' + args[1]]
3612 else:
3613 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003614 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003615
3616
thestig@chromium.org44202a22014-03-11 19:22:18 +00003617def CMDlint(parser, args):
3618 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003619 parser.add_option('--filter', action='append', metavar='-x,+y',
3620 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003621 auth.add_auth_options(parser)
3622 options, args = parser.parse_args(args)
3623 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003624
3625 # Access to a protected member _XX of a client class
3626 # pylint: disable=W0212
3627 try:
3628 import cpplint
3629 import cpplint_chromium
3630 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003631 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003632 return 1
3633
3634 # Change the current working directory before calling lint so that it
3635 # shows the correct base.
3636 previous_cwd = os.getcwd()
3637 os.chdir(settings.GetRoot())
3638 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003639 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003640 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3641 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003642 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003643 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003644 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003645
3646 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003647 command = args + files
3648 if options.filter:
3649 command = ['--filter=' + ','.join(options.filter)] + command
3650 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003651
3652 white_regex = re.compile(settings.GetLintRegex())
3653 black_regex = re.compile(settings.GetLintIgnoreRegex())
3654 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3655 for filename in filenames:
3656 if white_regex.match(filename):
3657 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003658 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003659 else:
3660 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3661 extra_check_functions)
3662 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003663 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003664 finally:
3665 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003666 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003667 if cpplint._cpplint_state.error_count != 0:
3668 return 1
3669 return 0
3670
3671
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003672def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003673 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003674 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003675 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003676 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003677 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003678 auth.add_auth_options(parser)
3679 options, args = parser.parse_args(args)
3680 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681
sbc@chromium.org71437c02015-04-09 19:29:40 +00003682 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003683 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003684 return 1
3685
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003686 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003687 if args:
3688 base_branch = args[0]
3689 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003690 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003691 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003692
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003693 cl.RunHook(
3694 committing=not options.upload,
3695 may_prompt=False,
3696 verbose=options.verbose,
3697 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003698 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003699
3700
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003701def GenerateGerritChangeId(message):
3702 """Returns Ixxxxxx...xxx change id.
3703
3704 Works the same way as
3705 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3706 but can be called on demand on all platforms.
3707
3708 The basic idea is to generate git hash of a state of the tree, original commit
3709 message, author/committer info and timestamps.
3710 """
3711 lines = []
3712 tree_hash = RunGitSilent(['write-tree'])
3713 lines.append('tree %s' % tree_hash.strip())
3714 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3715 if code == 0:
3716 lines.append('parent %s' % parent.strip())
3717 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3718 lines.append('author %s' % author.strip())
3719 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3720 lines.append('committer %s' % committer.strip())
3721 lines.append('')
3722 # Note: Gerrit's commit-hook actually cleans message of some lines and
3723 # whitespace. This code is not doing this, but it clearly won't decrease
3724 # entropy.
3725 lines.append(message)
3726 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3727 stdin='\n'.join(lines))
3728 return 'I%s' % change_hash.strip()
3729
3730
wittman@chromium.org455dc922015-01-26 20:15:50 +00003731def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3732 """Computes the remote branch ref to use for the CL.
3733
3734 Args:
3735 remote (str): The git remote for the CL.
3736 remote_branch (str): The git remote branch for the CL.
3737 target_branch (str): The target branch specified by the user.
3738 pending_prefix (str): The pending prefix from the settings.
3739 """
3740 if not (remote and remote_branch):
3741 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003742
wittman@chromium.org455dc922015-01-26 20:15:50 +00003743 if target_branch:
3744 # Cannonicalize branch references to the equivalent local full symbolic
3745 # refs, which are then translated into the remote full symbolic refs
3746 # below.
3747 if '/' not in target_branch:
3748 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3749 else:
3750 prefix_replacements = (
3751 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3752 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3753 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3754 )
3755 match = None
3756 for regex, replacement in prefix_replacements:
3757 match = re.search(regex, target_branch)
3758 if match:
3759 remote_branch = target_branch.replace(match.group(0), replacement)
3760 break
3761 if not match:
3762 # This is a branch path but not one we recognize; use as-is.
3763 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003764 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3765 # Handle the refs that need to land in different refs.
3766 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003767
wittman@chromium.org455dc922015-01-26 20:15:50 +00003768 # Create the true path to the remote branch.
3769 # Does the following translation:
3770 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3771 # * refs/remotes/origin/master -> refs/heads/master
3772 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3773 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3774 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3775 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3776 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3777 'refs/heads/')
3778 elif remote_branch.startswith('refs/remotes/branch-heads'):
3779 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3780 # If a pending prefix exists then replace refs/ with it.
3781 if pending_prefix:
3782 remote_branch = remote_branch.replace('refs/', pending_prefix)
3783 return remote_branch
3784
3785
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003786def cleanup_list(l):
3787 """Fixes a list so that comma separated items are put as individual items.
3788
3789 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3790 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3791 """
3792 items = sum((i.split(',') for i in l), [])
3793 stripped_items = (i.strip() for i in items)
3794 return sorted(filter(None, stripped_items))
3795
3796
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003797@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003798def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003799 """Uploads the current changelist to codereview.
3800
3801 Can skip dependency patchset uploads for a branch by running:
3802 git config branch.branch_name.skip-deps-uploads True
3803 To unset run:
3804 git config --unset branch.branch_name.skip-deps-uploads
3805 Can also set the above globally by using the --global flag.
3806 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003807 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3808 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003809 parser.add_option('--bypass-watchlists', action='store_true',
3810 dest='bypass_watchlists',
3811 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003812 parser.add_option('-f', action='store_true', dest='force',
3813 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003814 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003815 parser.add_option('-b', '--bug',
3816 help='pre-populate the bug number(s) for this issue. '
3817 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003818 parser.add_option('--message-file', dest='message_file',
3819 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003820 parser.add_option('-t', dest='title',
3821 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003822 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003823 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003824 help='reviewer email addresses')
3825 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003826 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003827 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003828 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003829 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003830 parser.add_option('--emulate_svn_auto_props',
3831 '--emulate-svn-auto-props',
3832 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003833 dest="emulate_svn_auto_props",
3834 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003835 parser.add_option('-c', '--use-commit-queue', action='store_true',
3836 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003837 parser.add_option('--private', action='store_true',
3838 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003839 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003840 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003841 metavar='TARGET',
3842 help='Apply CL to remote ref TARGET. ' +
3843 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003844 parser.add_option('--squash', action='store_true',
3845 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003846 parser.add_option('--no-squash', action='store_true',
3847 help='Don\'t squash multiple commits into one ' +
3848 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003849 parser.add_option('--email', default=None,
3850 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003851 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3852 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003853 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3854 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003855 help='Send the patchset to do a CQ dry run right after '
3856 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003857 parser.add_option('--dependencies', action='store_true',
3858 help='Uploads CLs of all the local branches that depend on '
3859 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003860
rmistry@google.com2dd99862015-06-22 12:22:18 +00003861 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003862 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003863 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003864 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003865 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003866 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003867 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003868
sbc@chromium.org71437c02015-04-09 19:29:40 +00003869 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003870 return 1
3871
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003872 options.reviewers = cleanup_list(options.reviewers)
3873 options.cc = cleanup_list(options.cc)
3874
tandriib80458a2016-06-23 12:20:07 -07003875 if options.message_file:
3876 if options.message:
3877 parser.error('only one of --message and --message-file allowed.')
3878 options.message = gclient_utils.FileRead(options.message_file)
3879 options.message_file = None
3880
tandrii4d0545a2016-07-06 03:56:49 -07003881 if options.cq_dry_run and options.use_commit_queue:
3882 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3883
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003884 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3885 settings.GetIsGerrit()
3886
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003887 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003888 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003889
3890
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003891def IsSubmoduleMergeCommit(ref):
3892 # When submodules are added to the repo, we expect there to be a single
3893 # non-git-svn merge commit at remote HEAD with a signature comment.
3894 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003895 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003896 return RunGit(cmd) != ''
3897
3898
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003899def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003900 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003901
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003902 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3903 upstream and closes the issue automatically and atomically.
3904
3905 Otherwise (in case of Rietveld):
3906 Squashes branch into a single commit.
3907 Updates changelog with metadata (e.g. pointer to review).
3908 Pushes/dcommits the code upstream.
3909 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910 """
3911 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3912 help='bypass upload presubmit hook')
3913 parser.add_option('-m', dest='message',
3914 help="override review description")
3915 parser.add_option('-f', action='store_true', dest='force',
3916 help="force yes to questions (don't prompt)")
3917 parser.add_option('-c', dest='contributor',
3918 help="external contributor for patch (appended to " +
3919 "description and used as author for git). Should be " +
3920 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003921 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003922 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003924 auth_config = auth.extract_auth_config_from_options(options)
3925
3926 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003927
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003928 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3929 if cl.IsGerrit():
3930 if options.message:
3931 # This could be implemented, but it requires sending a new patch to
3932 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3933 # Besides, Gerrit has the ability to change the commit message on submit
3934 # automatically, thus there is no need to support this option (so far?).
3935 parser.error('-m MESSAGE option is not supported for Gerrit.')
3936 if options.contributor:
3937 parser.error(
3938 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3939 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3940 'the contributor\'s "name <email>". If you can\'t upload such a '
3941 'commit for review, contact your repository admin and request'
3942 '"Forge-Author" permission.')
3943 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3944 options.verbose)
3945
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003946 current = cl.GetBranch()
3947 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3948 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003949 print()
3950 print('Attempting to push branch %r into another local branch!' % current)
3951 print()
3952 print('Either reparent this branch on top of origin/master:')
3953 print(' git reparent-branch --root')
3954 print()
3955 print('OR run `git rebase-update` if you think the parent branch is ')
3956 print('already committed.')
3957 print()
3958 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003959 return 1
3960
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003961 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003962 # Default to merging against our best guess of the upstream branch.
3963 args = [cl.GetUpstreamBranch()]
3964
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003965 if options.contributor:
3966 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003967 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003968 return 1
3969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003970 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003971 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003972
sbc@chromium.org71437c02015-04-09 19:29:40 +00003973 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974 return 1
3975
3976 # This rev-list syntax means "show all commits not in my branch that
3977 # are in base_branch".
3978 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3979 base_branch]).splitlines()
3980 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003981 print('Base branch "%s" has %d commits '
3982 'not in this branch.' % (base_branch, len(upstream_commits)))
3983 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984 return 1
3985
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003986 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003987 svn_head = None
3988 if cmd == 'dcommit' or base_has_submodules:
3989 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3990 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003993 # If the base_head is a submodule merge commit, the first parent of the
3994 # base_head should be a git-svn commit, which is what we're interested in.
3995 base_svn_head = base_branch
3996 if base_has_submodules:
3997 base_svn_head += '^1'
3998
3999 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004001 print('This branch has %d additional commits not upstreamed yet.'
4002 % len(extra_commits.splitlines()))
4003 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4004 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005 return 1
4006
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004007 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004008 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004009 author = None
4010 if options.contributor:
4011 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004012 hook_results = cl.RunHook(
4013 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004014 may_prompt=not options.force,
4015 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004016 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004017 if not hook_results.should_continue():
4018 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004019
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004020 # Check the tree status if the tree status URL is set.
4021 status = GetTreeStatus()
4022 if 'closed' == status:
4023 print('The tree is closed. Please wait for it to reopen. Use '
4024 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4025 return 1
4026 elif 'unknown' == status:
4027 print('Unable to determine tree status. Please verify manually and '
4028 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4029 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004030
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004031 change_desc = ChangeDescription(options.message)
4032 if not change_desc.description and cl.GetIssue():
4033 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004034
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004035 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004036 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004037 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004038 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004039 print('No description set.')
4040 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004041 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004042
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004043 # Keep a separate copy for the commit message, because the commit message
4044 # contains the link to the Rietveld issue, while the Rietveld message contains
4045 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004046 # Keep a separate copy for the commit message.
4047 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004048 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004049
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004050 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004051 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004052 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004053 # after it. Add a period on a new line to circumvent this. Also add a space
4054 # before the period to make sure that Gitiles continues to correctly resolve
4055 # the URL.
4056 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004057 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004058 commit_desc.append_footer('Patch from %s.' % options.contributor)
4059
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004060 print('Description:')
4061 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004062
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004063 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004064 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004065 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004067 # We want to squash all this branch's commits into one commit with the proper
4068 # description. We do this by doing a "reset --soft" to the base branch (which
4069 # keeps the working copy the same), then dcommitting that. If origin/master
4070 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4071 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004073 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4074 # Delete the branches if they exist.
4075 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4076 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4077 result = RunGitWithCode(showref_cmd)
4078 if result[0] == 0:
4079 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080
4081 # We might be in a directory that's present in this branch but not in the
4082 # trunk. Move up to the top of the tree so that git commands that expect a
4083 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004084 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004085 if rel_base_path:
4086 os.chdir(rel_base_path)
4087
4088 # Stuff our change into the merge branch.
4089 # We wrap in a try...finally block so if anything goes wrong,
4090 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004091 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004092 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004093 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004094 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004095 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004096 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004097 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004098 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004099 RunGit(
4100 [
4101 'commit', '--author', options.contributor,
4102 '-m', commit_desc.description,
4103 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004104 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004105 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004106 if base_has_submodules:
4107 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4108 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4109 RunGit(['checkout', CHERRY_PICK_BRANCH])
4110 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004111 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004112 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004113 mirror = settings.GetGitMirror(remote)
4114 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004115 pending_prefix = settings.GetPendingRefPrefix()
4116 if not pending_prefix or branch.startswith(pending_prefix):
4117 # If not using refs/pending/heads/* at all, or target ref is already set
4118 # to pending, then push to the target ref directly.
4119 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004120 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004121 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004122 else:
4123 # Cherry-pick the change on top of pending ref and then push it.
4124 assert branch.startswith('refs/'), branch
4125 assert pending_prefix[-1] == '/', pending_prefix
4126 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004127 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004128 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004129 if retcode == 0:
4130 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004131 else:
4132 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004133 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004134 'svn', 'dcommit',
4135 '-C%s' % options.similarity,
4136 '--no-rebase', '--rmdir',
4137 ]
4138 if settings.GetForceHttpsCommitUrl():
4139 # Allow forcing https commit URLs for some projects that don't allow
4140 # committing to http URLs (like Google Code).
4141 remote_url = cl.GetGitSvnRemoteUrl()
4142 if urlparse.urlparse(remote_url).scheme == 'http':
4143 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004144 cmd_args.append('--commit-url=%s' % remote_url)
4145 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004146 if 'Committed r' in output:
4147 revision = re.match(
4148 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4149 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004150 finally:
4151 # And then swap back to the original branch and clean up.
4152 RunGit(['checkout', '-q', cl.GetBranch()])
4153 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004154 if base_has_submodules:
4155 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004156
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004157 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004158 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004159 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004160
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004161 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004162 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004163 try:
4164 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4165 # We set pushed_to_pending to False, since it made it all the way to the
4166 # real ref.
4167 pushed_to_pending = False
4168 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004169 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004170
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004171 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004172 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004173 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004174 if not to_pending:
4175 if viewvc_url and revision:
4176 change_desc.append_footer(
4177 'Committed: %s%s' % (viewvc_url, revision))
4178 elif revision:
4179 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print('Closing issue '
4181 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004182 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004183 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004184 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004185 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004186 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004187 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004188 if options.bypass_hooks:
4189 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4190 else:
4191 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004192 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004193
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004194 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004195 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004196 print('The commit is in the pending queue (%s).' % pending_ref)
4197 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4198 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004199
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004200 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4201 if os.path.isfile(hook):
4202 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004203
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004204 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004205
4206
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004207def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004208 print()
4209 print('Waiting for commit to be landed on %s...' % real_ref)
4210 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004211 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4212 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004213 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004214
4215 loop = 0
4216 while True:
4217 sys.stdout.write('fetching (%d)... \r' % loop)
4218 sys.stdout.flush()
4219 loop += 1
4220
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004221 if mirror:
4222 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004223 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4224 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4225 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4226 for commit in commits.splitlines():
4227 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004228 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004229 return commit
4230
4231 current_rev = to_rev
4232
4233
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004234def PushToGitPending(remote, pending_ref, upstream_ref):
4235 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4236
4237 Returns:
4238 (retcode of last operation, output log of last operation).
4239 """
4240 assert pending_ref.startswith('refs/'), pending_ref
4241 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4242 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4243 code = 0
4244 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004245 max_attempts = 3
4246 attempts_left = max_attempts
4247 while attempts_left:
4248 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004249 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004250 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004251
4252 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004253 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004254 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004255 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004256 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004257 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004258 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004259 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004260 continue
4261
4262 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004263 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004264 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004265 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004266 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004267 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4268 'the following files have merge conflicts:' % pending_ref)
4269 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4270 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004271 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004272 return code, out
4273
4274 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004275 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004276 code, out = RunGitWithCode(
4277 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4278 if code == 0:
4279 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004280 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004281 return code, out
4282
vapiera7fbd5a2016-06-16 09:17:49 -07004283 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004284 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004285 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004286 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004287 print('Fatal push error. Make sure your .netrc credentials and git '
4288 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004289 return code, out
4290
vapiera7fbd5a2016-06-16 09:17:49 -07004291 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004292 return code, out
4293
4294
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004295def IsFatalPushFailure(push_stdout):
4296 """True if retrying push won't help."""
4297 return '(prohibited by Gerrit)' in push_stdout
4298
4299
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004300@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004302 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004303 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004304 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004305 # If it looks like previous commits were mirrored with git-svn.
4306 message = """This repository appears to be a git-svn mirror, but no
4307upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4308 else:
4309 message = """This doesn't appear to be an SVN repository.
4310If your project has a true, writeable git repository, you probably want to run
4311'git cl land' instead.
4312If your project has a git mirror of an upstream SVN master, you probably need
4313to run 'git svn init'.
4314
4315Using the wrong command might cause your commit to appear to succeed, and the
4316review to be closed, without actually landing upstream. If you choose to
4317proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004318 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004319 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004320 # TODO(tandrii): kill this post SVN migration with
4321 # https://codereview.chromium.org/2076683002
4322 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4323 'Please let us know of this project you are committing to:'
4324 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004325 return SendUpstream(parser, args, 'dcommit')
4326
4327
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004328@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004329def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004330 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004331 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004332 print('This appears to be an SVN repository.')
4333 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004334 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004335 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004336 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004337
4338
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004339@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004340def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004341 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004342 parser.add_option('-b', dest='newbranch',
4343 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004344 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004345 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004346 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4347 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004348 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004349 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004350 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004351 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004352 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004353 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004354
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004355
4356 group = optparse.OptionGroup(
4357 parser,
4358 'Options for continuing work on the current issue uploaded from a '
4359 'different clone (e.g. different machine). Must be used independently '
4360 'from the other options. No issue number should be specified, and the '
4361 'branch must have an issue number associated with it')
4362 group.add_option('--reapply', action='store_true', dest='reapply',
4363 help='Reset the branch and reapply the issue.\n'
4364 'CAUTION: This will undo any local changes in this '
4365 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004366
4367 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004368 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004369 parser.add_option_group(group)
4370
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004371 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004372 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004373 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004374 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004375 auth_config = auth.extract_auth_config_from_options(options)
4376
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004377
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004378 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004379 if options.newbranch:
4380 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004381 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004382 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004383
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004384 cl = Changelist(auth_config=auth_config,
4385 codereview=options.forced_codereview)
4386 if not cl.GetIssue():
4387 parser.error('current branch must have an associated issue')
4388
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004389 upstream = cl.GetUpstreamBranch()
4390 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004391 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004392
4393 RunGit(['reset', '--hard', upstream])
4394 if options.pull:
4395 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004396
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004397 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4398 options.directory)
4399
4400 if len(args) != 1 or not args[0]:
4401 parser.error('Must specify issue number or url')
4402
4403 # We don't want uncommitted changes mixed up with the patch.
4404 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004405 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004407 if options.newbranch:
4408 if options.force:
4409 RunGit(['branch', '-D', options.newbranch],
4410 stderr=subprocess2.PIPE, error_ok=True)
4411 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004412 elif not GetCurrentBranch():
4413 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004414
4415 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4416
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004417 if cl.IsGerrit():
4418 if options.reject:
4419 parser.error('--reject is not supported with Gerrit codereview.')
4420 if options.nocommit:
4421 parser.error('--nocommit is not supported with Gerrit codereview.')
4422 if options.directory:
4423 parser.error('--directory is not supported with Gerrit codereview.')
4424
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004425 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004426 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004427
4428
4429def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004430 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004431 # Provide a wrapper for git svn rebase to help avoid accidental
4432 # git svn dcommit.
4433 # It's the only command that doesn't use parser at all since we just defer
4434 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004435
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004436 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437
4438
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004439def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004440 """Fetches the tree status and returns either 'open', 'closed',
4441 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004442 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004443 if url:
4444 status = urllib2.urlopen(url).read().lower()
4445 if status.find('closed') != -1 or status == '0':
4446 return 'closed'
4447 elif status.find('open') != -1 or status == '1':
4448 return 'open'
4449 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004450 return 'unset'
4451
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004452
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004453def GetTreeStatusReason():
4454 """Fetches the tree status from a json url and returns the message
4455 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004456 url = settings.GetTreeStatusUrl()
4457 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004458 connection = urllib2.urlopen(json_url)
4459 status = json.loads(connection.read())
4460 connection.close()
4461 return status['message']
4462
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004463
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004464def GetBuilderMaster(bot_list):
4465 """For a given builder, fetch the master from AE if available."""
4466 map_url = 'https://builders-map.appspot.com/'
4467 try:
4468 master_map = json.load(urllib2.urlopen(map_url))
4469 except urllib2.URLError as e:
4470 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4471 (map_url, e))
4472 except ValueError as e:
4473 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4474 if not master_map:
4475 return None, 'Failed to build master map.'
4476
4477 result_master = ''
4478 for bot in bot_list:
4479 builder = bot.split(':', 1)[0]
4480 master_list = master_map.get(builder, [])
4481 if not master_list:
4482 return None, ('No matching master for builder %s.' % builder)
4483 elif len(master_list) > 1:
4484 return None, ('The builder name %s exists in multiple masters %s.' %
4485 (builder, master_list))
4486 else:
4487 cur_master = master_list[0]
4488 if not result_master:
4489 result_master = cur_master
4490 elif result_master != cur_master:
4491 return None, 'The builders do not belong to the same master.'
4492 return result_master, None
4493
4494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004495def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004496 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004497 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004498 status = GetTreeStatus()
4499 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004500 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004501 return 2
4502
vapiera7fbd5a2016-06-16 09:17:49 -07004503 print('The tree is %s' % status)
4504 print()
4505 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004506 if status != 'open':
4507 return 1
4508 return 0
4509
4510
maruel@chromium.org15192402012-09-06 12:38:29 +00004511def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004512 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004513 group = optparse.OptionGroup(parser, "Try job options")
4514 group.add_option(
4515 "-b", "--bot", action="append",
4516 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4517 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004518 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004519 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004520 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004521 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004522 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004523 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004524 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004525 "-r", "--revision",
4526 help="Revision to use for the try job; default: the "
4527 "revision will be determined by the try server; see "
4528 "its waterfall for more info")
4529 group.add_option(
4530 "-c", "--clobber", action="store_true", default=False,
4531 help="Force a clobber before building; e.g. don't do an "
4532 "incremental build")
4533 group.add_option(
4534 "--project",
4535 help="Override which project to use. Projects are defined "
4536 "server-side to define what default bot set to use")
4537 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004538 "-p", "--property", dest="properties", action="append", default=[],
4539 help="Specify generic properties in the form -p key1=value1 -p "
4540 "key2=value2 etc (buildbucket only). The value will be treated as "
4541 "json if decodable, or as string otherwise.")
4542 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004543 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004544 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004545 "--use-rietveld", action="store_true", default=False,
4546 help="Use Rietveld to trigger try jobs.")
4547 group.add_option(
4548 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4549 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004550 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004551 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004552 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004553 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004554
machenbach@chromium.org45453142015-09-15 08:45:22 +00004555 if options.use_rietveld and options.properties:
4556 parser.error('Properties can only be specified with buildbucket')
4557
4558 # Make sure that all properties are prop=value pairs.
4559 bad_params = [x for x in options.properties if '=' not in x]
4560 if bad_params:
4561 parser.error('Got properties with missing "=": %s' % bad_params)
4562
maruel@chromium.org15192402012-09-06 12:38:29 +00004563 if args:
4564 parser.error('Unknown arguments: %s' % args)
4565
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004566 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004567 if not cl.GetIssue():
4568 parser.error('Need to upload first')
4569
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004570 if cl.IsGerrit():
4571 parser.error(
4572 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4573 'If your project has Commit Queue, dry run is a workaround:\n'
4574 ' git cl set-commit --dry-run')
4575 # Code below assumes Rietveld issue.
4576 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4577
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004578 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004579 if props.get('closed'):
4580 parser.error('Cannot send tryjobs for a closed CL')
4581
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004582 if props.get('private'):
4583 parser.error('Cannot use trybots with private issue')
4584
maruel@chromium.org15192402012-09-06 12:38:29 +00004585 if not options.name:
4586 options.name = cl.GetBranch()
4587
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004588 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004589 options.master, err_msg = GetBuilderMaster(options.bot)
4590 if err_msg:
4591 parser.error('Tryserver master cannot be found because: %s\n'
4592 'Please manually specify the tryserver master'
4593 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004594
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004595 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004596 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004597 if not options.bot:
4598 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004599
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004600 # Get try masters from PRESUBMIT.py files.
4601 masters = presubmit_support.DoGetTryMasters(
4602 change,
4603 change.LocalPaths(),
4604 settings.GetRoot(),
4605 None,
4606 None,
4607 options.verbose,
4608 sys.stdout)
4609 if masters:
4610 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004611
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004612 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4613 options.bot = presubmit_support.DoGetTrySlaves(
4614 change,
4615 change.LocalPaths(),
4616 settings.GetRoot(),
4617 None,
4618 None,
4619 options.verbose,
4620 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004621
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004622 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004623 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004624
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004625 builders_and_tests = {}
4626 # TODO(machenbach): The old style command-line options don't support
4627 # multiple try masters yet.
4628 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4629 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4630
4631 for bot in old_style:
4632 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004633 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004634 elif ',' in bot:
4635 parser.error('Specify one bot per --bot flag')
4636 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004637 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004638
4639 for bot, tests in new_style:
4640 builders_and_tests.setdefault(bot, []).extend(tests)
4641
4642 # Return a master map with one master to be backwards compatible. The
4643 # master name defaults to an empty string, which will cause the master
4644 # not to be set on rietveld (deprecated).
4645 return {options.master: builders_and_tests}
4646
4647 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004648 if not masters:
4649 # Default to triggering Dry Run (see http://crbug.com/625697).
4650 if options.verbose:
4651 print('git cl try with no bots now defaults to CQ Dry Run.')
4652 try:
4653 cl.SetCQState(_CQState.DRY_RUN)
4654 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4655 return 0
4656 except KeyboardInterrupt:
4657 raise
4658 except:
4659 print('WARNING: failed to trigger CQ Dry Run.\n'
4660 'Either:\n'
4661 ' * your project has no CQ\n'
4662 ' * you don\'t have permission to trigger Dry Run\n'
4663 ' * bug in this code (see stack trace below).\n'
4664 'Consider specifying which bots to trigger manually '
4665 'or asking your project owners for permissions '
4666 'or contacting Chrome Infrastructure team at '
4667 'https://www.chromium.org/infra\n\n')
4668 # Still raise exception so that stack trace is printed.
4669 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004670
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004671 for builders in masters.itervalues():
4672 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004673 print('ERROR You are trying to send a job to a triggered bot. This type '
4674 'of bot requires an\ninitial job from a parent (usually a builder).'
4675 ' Instead send your job to the parent.\n'
4676 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004677 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004678
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004679 patchset = cl.GetMostRecentPatchset()
4680 if patchset and patchset != cl.GetPatchset():
4681 print(
4682 '\nWARNING Mismatch between local config and server. Did a previous '
4683 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4684 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004685 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004686 try:
4687 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4688 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004689 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004690 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004691 except Exception as e:
4692 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004693 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4694 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004695 return 1
4696 else:
4697 try:
4698 cl.RpcServer().trigger_distributed_try_jobs(
4699 cl.GetIssue(), patchset, options.name, options.clobber,
4700 options.revision, masters)
4701 except urllib2.HTTPError as e:
4702 if e.code == 404:
4703 print('404 from rietveld; '
4704 'did you mean to use "git try" instead of "git cl try"?')
4705 return 1
4706 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004707
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004708 for (master, builders) in sorted(masters.iteritems()):
4709 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004710 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004711 length = max(len(builder) for builder in builders)
4712 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004713 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004714 return 0
4715
4716
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004717def CMDtry_results(parser, args):
4718 group = optparse.OptionGroup(parser, "Try job results options")
4719 group.add_option(
4720 "-p", "--patchset", type=int, help="patchset number if not current.")
4721 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004722 "--print-master", action='store_true', help="print master name as well.")
4723 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004724 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004725 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004726 group.add_option(
4727 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4728 help="Host of buildbucket. The default host is %default.")
4729 parser.add_option_group(group)
4730 auth.add_auth_options(parser)
4731 options, args = parser.parse_args(args)
4732 if args:
4733 parser.error('Unrecognized args: %s' % ' '.join(args))
4734
4735 auth_config = auth.extract_auth_config_from_options(options)
4736 cl = Changelist(auth_config=auth_config)
4737 if not cl.GetIssue():
4738 parser.error('Need to upload first')
4739
4740 if not options.patchset:
4741 options.patchset = cl.GetMostRecentPatchset()
4742 if options.patchset and options.patchset != cl.GetPatchset():
4743 print(
4744 '\nWARNING Mismatch between local config and server. Did a previous '
4745 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4746 'Continuing using\npatchset %s.\n' % options.patchset)
4747 try:
4748 jobs = fetch_try_jobs(auth_config, cl, options)
4749 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004750 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004751 return 1
4752 except Exception as e:
4753 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004754 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4755 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004756 return 1
4757 print_tryjobs(options, jobs)
4758 return 0
4759
4760
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004761@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004762def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004763 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004764 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004765 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004766 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004768 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004769 if args:
4770 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004771 branch = cl.GetBranch()
4772 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004773 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004774 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004775
4776 # Clear configured merge-base, if there is one.
4777 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004778 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004779 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004780 return 0
4781
4782
thestig@chromium.org00858c82013-12-02 23:08:03 +00004783def CMDweb(parser, args):
4784 """Opens the current CL in the web browser."""
4785 _, args = parser.parse_args(args)
4786 if args:
4787 parser.error('Unrecognized args: %s' % ' '.join(args))
4788
4789 issue_url = Changelist().GetIssueURL()
4790 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004791 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004792 return 1
4793
4794 webbrowser.open(issue_url)
4795 return 0
4796
4797
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004798def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004799 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004800 parser.add_option('-d', '--dry-run', action='store_true',
4801 help='trigger in dry run mode')
4802 parser.add_option('-c', '--clear', action='store_true',
4803 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004804 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004805 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004806 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004807 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004808 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004809 if args:
4810 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004811 if options.dry_run and options.clear:
4812 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4813
iannuccie53c9352016-08-17 14:40:40 -07004814 cl = Changelist(auth_config=auth_config, issue=options.issue,
4815 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004816 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004817 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004818 elif options.dry_run:
4819 state = _CQState.DRY_RUN
4820 else:
4821 state = _CQState.COMMIT
4822 if not cl.GetIssue():
4823 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004824 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004825 return 0
4826
4827
groby@chromium.org411034a2013-02-26 15:12:01 +00004828def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004829 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004830 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004831 auth.add_auth_options(parser)
4832 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004833 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004834 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004835 if args:
4836 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004837 cl = Changelist(auth_config=auth_config, issue=options.issue,
4838 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004839 # Ensure there actually is an issue to close.
4840 cl.GetDescription()
4841 cl.CloseIssue()
4842 return 0
4843
4844
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004845def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004846 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004847 auth.add_auth_options(parser)
4848 options, args = parser.parse_args(args)
4849 auth_config = auth.extract_auth_config_from_options(options)
4850 if args:
4851 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004852
4853 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004854 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004855 # Staged changes would be committed along with the patch from last
4856 # upload, hence counted toward the "last upload" side in the final
4857 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004858 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004859 return 1
4860
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004861 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004862 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004863 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004864 if not issue:
4865 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004866 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004867 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004868
4869 # Create a new branch based on the merge-base
4870 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004871 # Clear cached branch in cl object, to avoid overwriting original CL branch
4872 # properties.
4873 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004874 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004875 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004876 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004877 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004878 return rtn
4879
wychen@chromium.org06928532015-02-03 02:11:29 +00004880 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004881 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004882 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004883 finally:
4884 RunGit(['checkout', '-q', branch])
4885 RunGit(['branch', '-D', TMP_BRANCH])
4886
4887 return 0
4888
4889
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004890def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004891 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004892 parser.add_option(
4893 '--no-color',
4894 action='store_true',
4895 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004896 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004897 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004898 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004899
4900 author = RunGit(['config', 'user.email']).strip() or None
4901
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004902 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004903
4904 if args:
4905 if len(args) > 1:
4906 parser.error('Unknown args')
4907 base_branch = args[0]
4908 else:
4909 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004910 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004911
4912 change = cl.GetChange(base_branch, None)
4913 return owners_finder.OwnersFinder(
4914 [f.LocalPath() for f in
4915 cl.GetChange(base_branch, None).AffectedFiles()],
4916 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004917 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004918 disable_color=options.no_color).run()
4919
4920
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004921def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004922 """Generates a diff command."""
4923 # Generate diff for the current branch's changes.
4924 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4925 upstream_commit, '--' ]
4926
4927 if args:
4928 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004929 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004930 diff_cmd.append(arg)
4931 else:
4932 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004933
4934 return diff_cmd
4935
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004936def MatchingFileType(file_name, extensions):
4937 """Returns true if the file name ends with one of the given extensions."""
4938 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004939
enne@chromium.org555cfe42014-01-29 18:21:39 +00004940@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004941def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004942 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004943 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004944 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004945 parser.add_option('--full', action='store_true',
4946 help='Reformat the full content of all touched files')
4947 parser.add_option('--dry-run', action='store_true',
4948 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004949 parser.add_option('--python', action='store_true',
4950 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004951 parser.add_option('--diff', action='store_true',
4952 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004953 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004954
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004955 # git diff generates paths against the root of the repository. Change
4956 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004957 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004958 if rel_base_path:
4959 os.chdir(rel_base_path)
4960
digit@chromium.org29e47272013-05-17 17:01:46 +00004961 # Grab the merge-base commit, i.e. the upstream commit of the current
4962 # branch when it was created or the last time it was rebased. This is
4963 # to cover the case where the user may have called "git fetch origin",
4964 # moving the origin branch to a newer commit, but hasn't rebased yet.
4965 upstream_commit = None
4966 cl = Changelist()
4967 upstream_branch = cl.GetUpstreamBranch()
4968 if upstream_branch:
4969 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4970 upstream_commit = upstream_commit.strip()
4971
4972 if not upstream_commit:
4973 DieWithError('Could not find base commit for this branch. '
4974 'Are you in detached state?')
4975
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004976 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4977 diff_output = RunGit(changed_files_cmd)
4978 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004979 # Filter out files deleted by this CL
4980 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004981
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004982 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4983 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4984 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004985 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004986
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004987 top_dir = os.path.normpath(
4988 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4989
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004990 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4991 # formatted. This is used to block during the presubmit.
4992 return_value = 0
4993
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004994 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004995 # Locate the clang-format binary in the checkout
4996 try:
4997 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004998 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004999 DieWithError(e)
5000
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005001 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005002 cmd = [clang_format_tool]
5003 if not opts.dry_run and not opts.diff:
5004 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005005 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005006 if opts.diff:
5007 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005008 else:
5009 env = os.environ.copy()
5010 env['PATH'] = str(os.path.dirname(clang_format_tool))
5011 try:
5012 script = clang_format.FindClangFormatScriptInChromiumTree(
5013 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005014 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005015 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005016
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005017 cmd = [sys.executable, script, '-p0']
5018 if not opts.dry_run and not opts.diff:
5019 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005020
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005021 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5022 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005023
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005024 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5025 if opts.diff:
5026 sys.stdout.write(stdout)
5027 if opts.dry_run and len(stdout) > 0:
5028 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005029
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005030 # Similar code to above, but using yapf on .py files rather than clang-format
5031 # on C/C++ files
5032 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005033 yapf_tool = gclient_utils.FindExecutable('yapf')
5034 if yapf_tool is None:
5035 DieWithError('yapf not found in PATH')
5036
5037 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005038 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005039 cmd = [yapf_tool]
5040 if not opts.dry_run and not opts.diff:
5041 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005042 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005043 if opts.diff:
5044 sys.stdout.write(stdout)
5045 else:
5046 # TODO(sbc): yapf --lines mode still has some issues.
5047 # https://github.com/google/yapf/issues/154
5048 DieWithError('--python currently only works with --full')
5049
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005050 # Dart's formatter does not have the nice property of only operating on
5051 # modified chunks, so hard code full.
5052 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005053 try:
5054 command = [dart_format.FindDartFmtToolInChromiumTree()]
5055 if not opts.dry_run and not opts.diff:
5056 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005057 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005058
ppi@chromium.org6593d932016-03-03 15:41:15 +00005059 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005060 if opts.dry_run and stdout:
5061 return_value = 2
5062 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005063 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5064 'found in this checkout. Files in other languages are still '
5065 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005066
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005067 # Format GN build files. Always run on full build files for canonical form.
5068 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005069 cmd = ['gn', 'format' ]
5070 if opts.dry_run or opts.diff:
5071 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005072 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005073 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5074 shell=sys.platform == 'win32',
5075 cwd=top_dir)
5076 if opts.dry_run and gn_ret == 2:
5077 return_value = 2 # Not formatted.
5078 elif opts.diff and gn_ret == 2:
5079 # TODO this should compute and print the actual diff.
5080 print("This change has GN build file diff for " + gn_diff_file)
5081 elif gn_ret != 0:
5082 # For non-dry run cases (and non-2 return values for dry-run), a
5083 # nonzero error code indicates a failure, probably because the file
5084 # doesn't parse.
5085 DieWithError("gn format failed on " + gn_diff_file +
5086 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005087
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005088 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005089
5090
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005091@subcommand.usage('<codereview url or issue id>')
5092def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005093 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005094 _, args = parser.parse_args(args)
5095
5096 if len(args) != 1:
5097 parser.print_help()
5098 return 1
5099
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005100 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005101 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005102 parser.print_help()
5103 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005104 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005105
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005106 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005107 output = RunGit(['config', '--local', '--get-regexp',
5108 r'branch\..*\.%s' % issueprefix],
5109 error_ok=True)
5110 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005111 if issue == target_issue:
5112 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005113
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005114 branches = []
5115 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005116 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005117 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005118 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005119 return 1
5120 if len(branches) == 1:
5121 RunGit(['checkout', branches[0]])
5122 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005123 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005124 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005125 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005126 which = raw_input('Choose by index: ')
5127 try:
5128 RunGit(['checkout', branches[int(which)]])
5129 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005130 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005131 return 1
5132
5133 return 0
5134
5135
maruel@chromium.org29404b52014-09-08 22:58:00 +00005136def CMDlol(parser, args):
5137 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005138 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005139 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5140 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5141 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005142 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005143 return 0
5144
5145
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005146class OptionParser(optparse.OptionParser):
5147 """Creates the option parse and add --verbose support."""
5148 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005149 optparse.OptionParser.__init__(
5150 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005151 self.add_option(
5152 '-v', '--verbose', action='count', default=0,
5153 help='Use 2 times for more debugging info')
5154
5155 def parse_args(self, args=None, values=None):
5156 options, args = optparse.OptionParser.parse_args(self, args, values)
5157 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5158 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5159 return options, args
5160
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005161
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005162def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005163 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005164 print('\nYour python version %s is unsupported, please upgrade.\n' %
5165 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005166 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005167
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005168 # Reload settings.
5169 global settings
5170 settings = Settings()
5171
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005172 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005173 dispatcher = subcommand.CommandDispatcher(__name__)
5174 try:
5175 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005176 except auth.AuthenticationError as e:
5177 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005178 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005179 if e.code != 500:
5180 raise
5181 DieWithError(
5182 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5183 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005184 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005185
5186
5187if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005188 # These affect sys.stdout so do it outside of main() to simplify mocks in
5189 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005190 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005191 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005192 try:
5193 sys.exit(main(sys.argv[1:]))
5194 except KeyboardInterrupt:
5195 sys.stderr.write('interrupted\n')
5196 sys.exit(1)