blob: 99c101e8506ea70083dfe1840979021b8822ea5a [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070087 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000121 try:
tandrii5d48c322016-08-18 16:19:37 -0700122 (out, _), code = subprocess2.communicate(['git'] + args,
123 env=GetNoGitPagerEnv(),
124 stdout=subprocess2.PIPE,
125 stderr=stderr)
126 return code, out
127 except subprocess2.CalledProcessError as e:
128 logging.debug('Failed running %s', args)
129 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000133 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
tandrii5d48c322016-08-18 16:19:37 -0700159def _git_branch_config_key(branch, key):
160 """Helper method to return Git config key for a branch."""
161 assert branch, 'branch name is required to set git config for it'
162 return 'branch.%s.%s' % (branch, key)
163
164
165def _git_get_branch_config_value(key, default=None, value_type=str,
166 branch=False):
167 """Returns git config value of given or current branch if any.
168
169 Returns default in all other cases.
170 """
171 assert value_type in (int, str, bool)
172 if branch is False: # Distinguishing default arg value from None.
173 branch = GetCurrentBranch()
174
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000175 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700176 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000177
tandrii5d48c322016-08-18 16:19:37 -0700178 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700179 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700180 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700181 # git config also has --int, but apparently git config suffers from integer
182 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700183 args.append(_git_branch_config_key(branch, key))
184 code, out = RunGitWithCode(args)
185 if code == 0:
186 value = out.strip()
187 if value_type == int:
188 return int(value)
189 if value_type == bool:
190 return bool(value.lower() == 'true')
191 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000192 return default
193
194
tandrii5d48c322016-08-18 16:19:37 -0700195def _git_set_branch_config_value(key, value, branch=None, **kwargs):
196 """Sets the value or unsets if it's None of a git branch config.
197
198 Valid, though not necessarily existing, branch must be provided,
199 otherwise currently checked out branch is used.
200 """
201 if not branch:
202 branch = GetCurrentBranch()
203 assert branch, 'a branch name OR currently checked out branch is required'
204 args = ['config']
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()
tandrii5d48c322016-08-18 16:19:37 -0700211 else:
tandrii33a46ff2016-08-23 05:53:40 -0700212 # git config also has --int, but apparently git config suffers from integer
213 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700214 value = str(value)
215 args.append(_git_branch_config_key(branch, key))
216 if value is not None:
217 args.append(value)
218 RunGit(args, **kwargs)
219
220
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000221def add_git_similarity(parser):
222 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700223 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000224 help='Sets the percentage that a pair of files need to match in order to'
225 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000226 parser.add_option(
227 '--find-copies', action='store_true',
228 help='Allows git to look for copies.')
229 parser.add_option(
230 '--no-find-copies', action='store_false', dest='find_copies',
231 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000232
233 old_parser_args = parser.parse_args
234 def Parse(args):
235 options, args = old_parser_args(args)
236
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000237 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700238 options.similarity = _git_get_branch_config_value(
239 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000240 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241 print('Note: Saving similarity of %d%% in git config.'
242 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700243 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000244
iannucci@chromium.org79540052012-10-19 23:15:26 +0000245 options.similarity = max(0, min(options.similarity, 100))
246
247 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.find_copies = _git_get_branch_config_value(
249 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000250 else:
tandrii5d48c322016-08-18 16:19:37 -0700251 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000252
253 print('Using %d%% similarity for rename/copy detection. '
254 'Override with --similarity.' % options.similarity)
255
256 return options, args
257 parser.parse_args = Parse
258
259
machenbach@chromium.org45453142015-09-15 08:45:22 +0000260def _get_properties_from_options(options):
261 properties = dict(x.split('=', 1) for x in options.properties)
262 for key, val in properties.iteritems():
263 try:
264 properties[key] = json.loads(val)
265 except ValueError:
266 pass # If a value couldn't be evaluated, treat it as a string.
267 return properties
268
269
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000270def _prefix_master(master):
271 """Convert user-specified master name to full master name.
272
273 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
274 name, while the developers always use shortened master name
275 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
276 function does the conversion for buildbucket migration.
277 """
278 prefix = 'master.'
279 if master.startswith(prefix):
280 return master
281 return '%s%s' % (prefix, master)
282
283
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000284def _buildbucket_retry(operation_name, http, *args, **kwargs):
285 """Retries requests to buildbucket service and returns parsed json content."""
286 try_count = 0
287 while True:
288 response, content = http.request(*args, **kwargs)
289 try:
290 content_json = json.loads(content)
291 except ValueError:
292 content_json = None
293
294 # Buildbucket could return an error even if status==200.
295 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000296 error = content_json.get('error')
297 if error.get('code') == 403:
298 raise BuildbucketResponseException(
299 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000300 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000301 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000302 raise BuildbucketResponseException(msg)
303
304 if response.status == 200:
305 if not content_json:
306 raise BuildbucketResponseException(
307 'Buildbucket returns invalid json content: %s.\n'
308 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
309 content)
310 return content_json
311 if response.status < 500 or try_count >= 2:
312 raise httplib2.HttpLib2Error(content)
313
314 # status >= 500 means transient failures.
315 logging.debug('Transient errors when %s. Will retry.', operation_name)
316 time.sleep(0.5 + 1.5*try_count)
317 try_count += 1
318 assert False, 'unreachable'
319
320
machenbach@chromium.org45453142015-09-15 08:45:22 +0000321def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000322 rietveld_url = settings.GetDefaultServerUrl()
323 rietveld_host = urlparse.urlparse(rietveld_url).hostname
324 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
325 http = authenticator.authorize(httplib2.Http())
326 http.force_exception_to_status_code = True
327 issue_props = changelist.GetIssueProperties()
328 issue = changelist.GetIssue()
329 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000330 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332 buildbucket_put_url = (
333 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000334 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000335 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
336 hostname=rietveld_host,
337 issue=issue,
338 patch=patchset)
339
340 batch_req_body = {'builds': []}
341 print_text = []
342 print_text.append('Tried jobs on:')
343 for master, builders_and_tests in sorted(masters.iteritems()):
344 print_text.append('Master: %s' % master)
345 bucket = _prefix_master(master)
346 for builder, tests in sorted(builders_and_tests.iteritems()):
347 print_text.append(' %s: %s' % (builder, tests))
348 parameters = {
349 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000350 'changes': [{
351 'author': {'email': issue_props['owner_email']},
352 'revision': options.revision,
353 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 'properties': {
355 'category': category,
356 'issue': issue,
357 'master': master,
358 'patch_project': issue_props['project'],
359 'patch_storage': 'rietveld',
360 'patchset': patchset,
361 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000362 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 },
364 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000365 if 'presubmit' in builder.lower():
366 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000367 if tests:
368 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000369 if properties:
370 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371 if options.clobber:
372 parameters['properties']['clobber'] = True
373 batch_req_body['builds'].append(
374 {
375 'bucket': bucket,
376 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000377 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000378 'tags': ['builder:%s' % builder,
379 'buildset:%s' % buildset,
380 'master:%s' % master,
381 'user_agent:git_cl_try']
382 }
383 )
384
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000385 _buildbucket_retry(
386 'triggering tryjobs',
387 http,
388 buildbucket_put_url,
389 'PUT',
390 body=json.dumps(batch_req_body),
391 headers={'Content-Type': 'application/json'}
392 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000393 print_text.append('To see results here, run: git cl try-results')
394 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700395 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000396
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000397
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000398def fetch_try_jobs(auth_config, changelist, options):
399 """Fetches tryjobs from buildbucket.
400
401 Returns a map from build id to build info as json dictionary.
402 """
403 rietveld_url = settings.GetDefaultServerUrl()
404 rietveld_host = urlparse.urlparse(rietveld_url).hostname
405 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
406 if authenticator.has_cached_credentials():
407 http = authenticator.authorize(httplib2.Http())
408 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700409 print('Warning: Some results might be missing because %s' %
410 # Get the message on how to login.
411 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000412 http = httplib2.Http()
413
414 http.force_exception_to_status_code = True
415
416 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
417 hostname=rietveld_host,
418 issue=changelist.GetIssue(),
419 patch=options.patchset)
420 params = {'tag': 'buildset:%s' % buildset}
421
422 builds = {}
423 while True:
424 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
425 hostname=options.buildbucket_host,
426 params=urllib.urlencode(params))
427 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
428 for build in content.get('builds', []):
429 builds[build['id']] = build
430 if 'next_cursor' in content:
431 params['start_cursor'] = content['next_cursor']
432 else:
433 break
434 return builds
435
436
437def print_tryjobs(options, builds):
438 """Prints nicely result of fetch_try_jobs."""
439 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700440 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000441 return
442
443 # Make a copy, because we'll be modifying builds dictionary.
444 builds = builds.copy()
445 builder_names_cache = {}
446
447 def get_builder(b):
448 try:
449 return builder_names_cache[b['id']]
450 except KeyError:
451 try:
452 parameters = json.loads(b['parameters_json'])
453 name = parameters['builder_name']
454 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700455 print('WARNING: failed to get builder name for build %s: %s' % (
456 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000457 name = None
458 builder_names_cache[b['id']] = name
459 return name
460
461 def get_bucket(b):
462 bucket = b['bucket']
463 if bucket.startswith('master.'):
464 return bucket[len('master.'):]
465 return bucket
466
467 if options.print_master:
468 name_fmt = '%%-%ds %%-%ds' % (
469 max(len(str(get_bucket(b))) for b in builds.itervalues()),
470 max(len(str(get_builder(b))) for b in builds.itervalues()))
471 def get_name(b):
472 return name_fmt % (get_bucket(b), get_builder(b))
473 else:
474 name_fmt = '%%-%ds' % (
475 max(len(str(get_builder(b))) for b in builds.itervalues()))
476 def get_name(b):
477 return name_fmt % get_builder(b)
478
479 def sort_key(b):
480 return b['status'], b.get('result'), get_name(b), b.get('url')
481
482 def pop(title, f, color=None, **kwargs):
483 """Pop matching builds from `builds` dict and print them."""
484
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000485 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000486 colorize = str
487 else:
488 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
489
490 result = []
491 for b in builds.values():
492 if all(b.get(k) == v for k, v in kwargs.iteritems()):
493 builds.pop(b['id'])
494 result.append(b)
495 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700496 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000497 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700498 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000499
500 total = len(builds)
501 pop(status='COMPLETED', result='SUCCESS',
502 title='Successes:', color=Fore.GREEN,
503 f=lambda b: (get_name(b), b.get('url')))
504 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
505 title='Infra Failures:', color=Fore.MAGENTA,
506 f=lambda b: (get_name(b), b.get('url')))
507 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
508 title='Failures:', color=Fore.RED,
509 f=lambda b: (get_name(b), b.get('url')))
510 pop(status='COMPLETED', result='CANCELED',
511 title='Canceled:', color=Fore.MAGENTA,
512 f=lambda b: (get_name(b),))
513 pop(status='COMPLETED', result='FAILURE',
514 failure_reason='INVALID_BUILD_DEFINITION',
515 title='Wrong master/builder name:', color=Fore.MAGENTA,
516 f=lambda b: (get_name(b),))
517 pop(status='COMPLETED', result='FAILURE',
518 title='Other failures:',
519 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
520 pop(status='COMPLETED',
521 title='Other finished:',
522 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
523 pop(status='STARTED',
524 title='Started:', color=Fore.YELLOW,
525 f=lambda b: (get_name(b), b.get('url')))
526 pop(status='SCHEDULED',
527 title='Scheduled:',
528 f=lambda b: (get_name(b), 'id=%s' % b['id']))
529 # The last section is just in case buildbucket API changes OR there is a bug.
530 pop(title='Other:',
531 f=lambda b: (get_name(b), 'id=%s' % b['id']))
532 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700533 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534
535
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000536def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
537 """Return the corresponding git ref if |base_url| together with |glob_spec|
538 matches the full |url|.
539
540 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
541 """
542 fetch_suburl, as_ref = glob_spec.split(':')
543 if allow_wildcards:
544 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
545 if glob_match:
546 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
547 # "branches/{472,597,648}/src:refs/remotes/svn/*".
548 branch_re = re.escape(base_url)
549 if glob_match.group(1):
550 branch_re += '/' + re.escape(glob_match.group(1))
551 wildcard = glob_match.group(2)
552 if wildcard == '*':
553 branch_re += '([^/]*)'
554 else:
555 # Escape and replace surrounding braces with parentheses and commas
556 # with pipe symbols.
557 wildcard = re.escape(wildcard)
558 wildcard = re.sub('^\\\\{', '(', wildcard)
559 wildcard = re.sub('\\\\,', '|', wildcard)
560 wildcard = re.sub('\\\\}$', ')', wildcard)
561 branch_re += wildcard
562 if glob_match.group(3):
563 branch_re += re.escape(glob_match.group(3))
564 match = re.match(branch_re, url)
565 if match:
566 return re.sub('\*$', match.group(1), as_ref)
567
568 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
569 if fetch_suburl:
570 full_url = base_url + '/' + fetch_suburl
571 else:
572 full_url = base_url
573 if full_url == url:
574 return as_ref
575 return None
576
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000577
iannucci@chromium.org79540052012-10-19 23:15:26 +0000578def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000579 """Prints statistics about the change to the user."""
580 # --no-ext-diff is broken in some versions of Git, so try to work around
581 # this by overriding the environment (but there is still a problem if the
582 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000583 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000584 if 'GIT_EXTERNAL_DIFF' in env:
585 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000586
587 if find_copies:
588 similarity_options = ['--find-copies-harder', '-l100000',
589 '-C%s' % similarity]
590 else:
591 similarity_options = ['-M%s' % similarity]
592
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000593 try:
594 stdout = sys.stdout.fileno()
595 except AttributeError:
596 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000597 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000598 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000599 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000600 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000601
602
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000603class BuildbucketResponseException(Exception):
604 pass
605
606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607class Settings(object):
608 def __init__(self):
609 self.default_server = None
610 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000611 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 self.is_git_svn = None
613 self.svn_branch = None
614 self.tree_status_url = None
615 self.viewvc_url = None
616 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000617 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000618 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000619 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000620 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000621 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000622 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000623 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624
625 def LazyUpdateIfNeeded(self):
626 """Updates the settings from a codereview.settings file, if available."""
627 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000628 # The only value that actually changes the behavior is
629 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000630 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000631 error_ok=True
632 ).strip().lower()
633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000635 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000636 LoadCodereviewSettingsFromFile(cr_settings_file)
637 self.updated = True
638
639 def GetDefaultServerUrl(self, error_ok=False):
640 if not self.default_server:
641 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000642 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000643 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644 if error_ok:
645 return self.default_server
646 if not self.default_server:
647 error_message = ('Could not find settings file. You must configure '
648 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000649 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000650 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 return self.default_server
652
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000653 @staticmethod
654 def GetRelativeRoot():
655 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000656
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000658 if self.root is None:
659 self.root = os.path.abspath(self.GetRelativeRoot())
660 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000662 def GetGitMirror(self, remote='origin'):
663 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000664 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000665 if not os.path.isdir(local_url):
666 return None
667 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
668 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
669 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
670 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
671 if mirror.exists():
672 return mirror
673 return None
674
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675 def GetIsGitSvn(self):
676 """Return true if this repo looks like it's using git-svn."""
677 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000678 if self.GetPendingRefPrefix():
679 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
680 self.is_git_svn = False
681 else:
682 # If you have any "svn-remote.*" config keys, we think you're using svn.
683 self.is_git_svn = RunGitWithCode(
684 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685 return self.is_git_svn
686
687 def GetSVNBranch(self):
688 if self.svn_branch is None:
689 if not self.GetIsGitSvn():
690 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
691
692 # Try to figure out which remote branch we're based on.
693 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000694 # 1) iterate through our branch history and find the svn URL.
695 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696
697 # regexp matching the git-svn line that contains the URL.
698 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
699
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 # We don't want to go through all of history, so read a line from the
701 # pipe at a time.
702 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000703 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000704 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
705 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000706 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000707 for line in proc.stdout:
708 match = git_svn_re.match(line)
709 if match:
710 url = match.group(1)
711 proc.stdout.close() # Cut pipe.
712 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000714 if url:
715 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
716 remotes = RunGit(['config', '--get-regexp',
717 r'^svn-remote\..*\.url']).splitlines()
718 for remote in remotes:
719 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000721 remote = match.group(1)
722 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000723 rewrite_root = RunGit(
724 ['config', 'svn-remote.%s.rewriteRoot' % remote],
725 error_ok=True).strip()
726 if rewrite_root:
727 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000728 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000729 ['config', 'svn-remote.%s.fetch' % remote],
730 error_ok=True).strip()
731 if fetch_spec:
732 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
733 if self.svn_branch:
734 break
735 branch_spec = RunGit(
736 ['config', 'svn-remote.%s.branches' % remote],
737 error_ok=True).strip()
738 if branch_spec:
739 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
740 if self.svn_branch:
741 break
742 tag_spec = RunGit(
743 ['config', 'svn-remote.%s.tags' % remote],
744 error_ok=True).strip()
745 if tag_spec:
746 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
747 if self.svn_branch:
748 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000749
750 if not self.svn_branch:
751 DieWithError('Can\'t guess svn branch -- try specifying it on the '
752 'command line')
753
754 return self.svn_branch
755
756 def GetTreeStatusUrl(self, error_ok=False):
757 if not self.tree_status_url:
758 error_message = ('You must configure your tree status URL by running '
759 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 self.tree_status_url = self._GetRietveldConfig(
761 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 return self.tree_status_url
763
764 def GetViewVCUrl(self):
765 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000766 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 return self.viewvc_url
768
rmistry@google.com90752582014-01-14 21:04:50 +0000769 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000770 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000771
rmistry@google.com78948ed2015-07-08 23:09:57 +0000772 def GetIsSkipDependencyUpload(self, branch_name):
773 """Returns true if specified branch should skip dep uploads."""
774 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
775 error_ok=True)
776
rmistry@google.com5626a922015-02-26 14:03:30 +0000777 def GetRunPostUploadHook(self):
778 run_post_upload_hook = self._GetRietveldConfig(
779 'run-post-upload-hook', error_ok=True)
780 return run_post_upload_hook == "True"
781
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000782 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000783 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000784
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000785 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000786 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000787
ukai@chromium.orge8077812012-02-03 03:41:46 +0000788 def GetIsGerrit(self):
789 """Return true if this repo is assosiated with gerrit code review system."""
790 if self.is_gerrit is None:
791 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
792 return self.is_gerrit
793
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000794 def GetSquashGerritUploads(self):
795 """Return true if uploads to Gerrit should be squashed by default."""
796 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700797 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
798 if self.squash_gerrit_uploads is None:
799 # Default is squash now (http://crbug.com/611892#c23).
800 self.squash_gerrit_uploads = not (
801 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
802 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000803 return self.squash_gerrit_uploads
804
tandriia60502f2016-06-20 02:01:53 -0700805 def GetSquashGerritUploadsOverride(self):
806 """Return True or False if codereview.settings should be overridden.
807
808 Returns None if no override has been defined.
809 """
810 # See also http://crbug.com/611892#c23
811 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
812 error_ok=True).strip()
813 if result == 'true':
814 return True
815 if result == 'false':
816 return False
817 return None
818
tandrii@chromium.org28253532016-04-14 13:46:56 +0000819 def GetGerritSkipEnsureAuthenticated(self):
820 """Return True if EnsureAuthenticated should not be done for Gerrit
821 uploads."""
822 if self.gerrit_skip_ensure_authenticated is None:
823 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000824 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000825 error_ok=True).strip() == 'true')
826 return self.gerrit_skip_ensure_authenticated
827
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000828 def GetGitEditor(self):
829 """Return the editor specified in the git config, or None if none is."""
830 if self.git_editor is None:
831 self.git_editor = self._GetConfig('core.editor', error_ok=True)
832 return self.git_editor or None
833
thestig@chromium.org44202a22014-03-11 19:22:18 +0000834 def GetLintRegex(self):
835 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
836 DEFAULT_LINT_REGEX)
837
838 def GetLintIgnoreRegex(self):
839 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
840 DEFAULT_LINT_IGNORE_REGEX)
841
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000842 def GetProject(self):
843 if not self.project:
844 self.project = self._GetRietveldConfig('project', error_ok=True)
845 return self.project
846
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000847 def GetForceHttpsCommitUrl(self):
848 if not self.force_https_commit_url:
849 self.force_https_commit_url = self._GetRietveldConfig(
850 'force-https-commit-url', error_ok=True)
851 return self.force_https_commit_url
852
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000853 def GetPendingRefPrefix(self):
854 if not self.pending_ref_prefix:
855 self.pending_ref_prefix = self._GetRietveldConfig(
856 'pending-ref-prefix', error_ok=True)
857 return self.pending_ref_prefix
858
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000859 def _GetRietveldConfig(self, param, **kwargs):
860 return self._GetConfig('rietveld.' + param, **kwargs)
861
rmistry@google.com78948ed2015-07-08 23:09:57 +0000862 def _GetBranchConfig(self, branch_name, param, **kwargs):
863 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
864
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 def _GetConfig(self, param, **kwargs):
866 self.LazyUpdateIfNeeded()
867 return RunGit(['config', param], **kwargs).strip()
868
869
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870def ShortBranchName(branch):
871 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000872 return branch.replace('refs/heads/', '', 1)
873
874
875def GetCurrentBranchRef():
876 """Returns branch ref (e.g., refs/heads/master) or None."""
877 return RunGit(['symbolic-ref', 'HEAD'],
878 stderr=subprocess2.VOID, error_ok=True).strip() or None
879
880
881def GetCurrentBranch():
882 """Returns current branch or None.
883
884 For refs/heads/* branches, returns just last part. For others, full ref.
885 """
886 branchref = GetCurrentBranchRef()
887 if branchref:
888 return ShortBranchName(branchref)
889 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890
891
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000892class _CQState(object):
893 """Enum for states of CL with respect to Commit Queue."""
894 NONE = 'none'
895 DRY_RUN = 'dry_run'
896 COMMIT = 'commit'
897
898 ALL_STATES = [NONE, DRY_RUN, COMMIT]
899
900
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000901class _ParsedIssueNumberArgument(object):
902 def __init__(self, issue=None, patchset=None, hostname=None):
903 self.issue = issue
904 self.patchset = patchset
905 self.hostname = hostname
906
907 @property
908 def valid(self):
909 return self.issue is not None
910
911
912class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
913 def __init__(self, *args, **kwargs):
914 self.patch_url = kwargs.pop('patch_url', None)
915 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
916
917
918def ParseIssueNumberArgument(arg):
919 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
920 fail_result = _ParsedIssueNumberArgument()
921
922 if arg.isdigit():
923 return _ParsedIssueNumberArgument(issue=int(arg))
924 if not arg.startswith('http'):
925 return fail_result
926 url = gclient_utils.UpgradeToHttps(arg)
927 try:
928 parsed_url = urlparse.urlparse(url)
929 except ValueError:
930 return fail_result
931 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
932 tmp = cls.ParseIssueURL(parsed_url)
933 if tmp is not None:
934 return tmp
935 return fail_result
936
937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000939 """Changelist works with one changelist in local branch.
940
941 Supports two codereview backends: Rietveld or Gerrit, selected at object
942 creation.
943
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000944 Notes:
945 * Not safe for concurrent multi-{thread,process} use.
946 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -0700947 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000948 """
949
950 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
951 """Create a new ChangeList instance.
952
953 If issue is given, the codereview must be given too.
954
955 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
956 Otherwise, it's decided based on current configuration of the local branch,
957 with default being 'rietveld' for backwards compatibility.
958 See _load_codereview_impl for more details.
959
960 **kwargs will be passed directly to codereview implementation.
961 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000963 global settings
964 if not settings:
965 # Happens when git_cl.py is used as a utility library.
966 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000967
968 if issue:
969 assert codereview, 'codereview must be known, if issue is known'
970
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000971 self.branchref = branchref
972 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000973 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974 self.branch = ShortBranchName(self.branchref)
975 else:
976 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000978 self.lookedup_issue = False
979 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980 self.has_description = False
981 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000982 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000984 self.cc = None
985 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000986 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000987
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000988 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000989 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000990 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000991 assert self._codereview_impl
992 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000993
994 def _load_codereview_impl(self, codereview=None, **kwargs):
995 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000996 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
997 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
998 self._codereview = codereview
999 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001000 return
1001
1002 # Automatic selection based on issue number set for a current branch.
1003 # Rietveld takes precedence over Gerrit.
1004 assert not self.issue
1005 # Whether we find issue or not, we are doing the lookup.
1006 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001007 if self.GetBranch():
1008 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1009 issue = _git_get_branch_config_value(
1010 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1011 if issue:
1012 self._codereview = codereview
1013 self._codereview_impl = cls(self, **kwargs)
1014 self.issue = int(issue)
1015 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001016
1017 # No issue is set for this branch, so decide based on repo-wide settings.
1018 return self._load_codereview_impl(
1019 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1020 **kwargs)
1021
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001022 def IsGerrit(self):
1023 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001024
1025 def GetCCList(self):
1026 """Return the users cc'd on this CL.
1027
1028 Return is a string suitable for passing to gcl with the --cc flag.
1029 """
1030 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001031 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001032 more_cc = ','.join(self.watchers)
1033 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1034 return self.cc
1035
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001036 def GetCCListWithoutDefault(self):
1037 """Return the users cc'd on this CL excluding default ones."""
1038 if self.cc is None:
1039 self.cc = ','.join(self.watchers)
1040 return self.cc
1041
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001042 def SetWatchers(self, watchers):
1043 """Set the list of email addresses that should be cc'd based on the changed
1044 files in this CL.
1045 """
1046 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047
1048 def GetBranch(self):
1049 """Returns the short branch name, e.g. 'master'."""
1050 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001051 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001052 if not branchref:
1053 return None
1054 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 self.branch = ShortBranchName(self.branchref)
1056 return self.branch
1057
1058 def GetBranchRef(self):
1059 """Returns the full branch name, e.g. 'refs/heads/master'."""
1060 self.GetBranch() # Poke the lazy loader.
1061 return self.branchref
1062
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001063 def ClearBranch(self):
1064 """Clears cached branch data of this object."""
1065 self.branch = self.branchref = None
1066
tandrii5d48c322016-08-18 16:19:37 -07001067 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1068 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1069 kwargs['branch'] = self.GetBranch()
1070 return _git_get_branch_config_value(key, default, **kwargs)
1071
1072 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1073 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1074 assert self.GetBranch(), (
1075 'this CL must have an associated branch to %sset %s%s' %
1076 ('un' if value is None else '',
1077 key,
1078 '' if value is None else ' to %r' % value))
1079 kwargs['branch'] = self.GetBranch()
1080 return _git_set_branch_config_value(key, value, **kwargs)
1081
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001082 @staticmethod
1083 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001084 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 e.g. 'origin', 'refs/heads/master'
1086 """
1087 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001088 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001091 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001093 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1094 error_ok=True).strip()
1095 if upstream_branch:
1096 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001098 # Fall back on trying a git-svn upstream branch.
1099 if settings.GetIsGitSvn():
1100 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001102 # Else, try to guess the origin remote.
1103 remote_branches = RunGit(['branch', '-r']).split()
1104 if 'origin/master' in remote_branches:
1105 # Fall back on origin/master if it exits.
1106 remote = 'origin'
1107 upstream_branch = 'refs/heads/master'
1108 elif 'origin/trunk' in remote_branches:
1109 # Fall back on origin/trunk if it exists. Generally a shared
1110 # git-svn clone
1111 remote = 'origin'
1112 upstream_branch = 'refs/heads/trunk'
1113 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114 DieWithError(
1115 'Unable to determine default branch to diff against.\n'
1116 'Either pass complete "git diff"-style arguments, like\n'
1117 ' git cl upload origin/master\n'
1118 'or verify this branch is set up to track another \n'
1119 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120
1121 return remote, upstream_branch
1122
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001123 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001124 upstream_branch = self.GetUpstreamBranch()
1125 if not BranchExists(upstream_branch):
1126 DieWithError('The upstream for the current branch (%s) does not exist '
1127 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001128 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001129 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001130
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 def GetUpstreamBranch(self):
1132 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001133 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001135 upstream_branch = upstream_branch.replace('refs/heads/',
1136 'refs/remotes/%s/' % remote)
1137 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1138 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 self.upstream_branch = upstream_branch
1140 return self.upstream_branch
1141
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001142 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001143 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001144 remote, branch = None, self.GetBranch()
1145 seen_branches = set()
1146 while branch not in seen_branches:
1147 seen_branches.add(branch)
1148 remote, branch = self.FetchUpstreamTuple(branch)
1149 branch = ShortBranchName(branch)
1150 if remote != '.' or branch.startswith('refs/remotes'):
1151 break
1152 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001153 remotes = RunGit(['remote'], error_ok=True).split()
1154 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001155 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001156 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001157 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001158 logging.warning('Could not determine which remote this change is '
1159 'associated with, so defaulting to "%s". This may '
1160 'not be what you want. You may prevent this message '
1161 'by running "git svn info" as documented here: %s',
1162 self._remote,
1163 GIT_INSTRUCTIONS_URL)
1164 else:
1165 logging.warn('Could not determine which remote this change is '
1166 'associated with. You may prevent this message by '
1167 'running "git svn info" as documented here: %s',
1168 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 branch = 'HEAD'
1170 if branch.startswith('refs/remotes'):
1171 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001172 elif branch.startswith('refs/branch-heads/'):
1173 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001174 else:
1175 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001176 return self._remote
1177
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001178 def GitSanityChecks(self, upstream_git_obj):
1179 """Checks git repo status and ensures diff is from local commits."""
1180
sbc@chromium.org79706062015-01-14 21:18:12 +00001181 if upstream_git_obj is None:
1182 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001183 print('ERROR: unable to determine current branch (detached HEAD?)',
1184 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001185 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001186 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001187 return False
1188
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 # Verify the commit we're diffing against is in our current branch.
1190 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1191 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1192 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001193 print('ERROR: %s is not in the current branch. You may need to rebase '
1194 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001195 return False
1196
1197 # List the commits inside the diff, and verify they are all local.
1198 commits_in_diff = RunGit(
1199 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1200 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1201 remote_branch = remote_branch.strip()
1202 if code != 0:
1203 _, remote_branch = self.GetRemoteBranch()
1204
1205 commits_in_remote = RunGit(
1206 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1207
1208 common_commits = set(commits_in_diff) & set(commits_in_remote)
1209 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001210 print('ERROR: Your diff contains %d commits already in %s.\n'
1211 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1212 'the diff. If you are using a custom git flow, you can override'
1213 ' the reference used for this check with "git config '
1214 'gitcl.remotebranch <git-ref>".' % (
1215 len(common_commits), remote_branch, upstream_git_obj),
1216 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001217 return False
1218 return True
1219
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001220 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001221 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001222
1223 Returns None if it is not set.
1224 """
tandrii5d48c322016-08-18 16:19:37 -07001225 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001226
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001227 def GetGitSvnRemoteUrl(self):
1228 """Return the configured git-svn remote URL parsed from git svn info.
1229
1230 Returns None if it is not set.
1231 """
1232 # URL is dependent on the current directory.
1233 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1234 if data:
1235 keys = dict(line.split(': ', 1) for line in data.splitlines()
1236 if ': ' in line)
1237 return keys.get('URL', None)
1238 return None
1239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 def GetRemoteUrl(self):
1241 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1242
1243 Returns None if there is no remote.
1244 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001245 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001246 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1247
1248 # If URL is pointing to a local directory, it is probably a git cache.
1249 if os.path.isdir(url):
1250 url = RunGit(['config', 'remote.%s.url' % remote],
1251 error_ok=True,
1252 cwd=url).strip()
1253 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001255 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001256 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001257 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001258 self.issue = self._GitGetBranchConfigValue(
1259 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001260 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 return self.issue
1262
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263 def GetIssueURL(self):
1264 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001265 issue = self.GetIssue()
1266 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001267 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001268 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269
1270 def GetDescription(self, pretty=False):
1271 if not self.has_description:
1272 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001273 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 self.has_description = True
1275 if pretty:
1276 wrapper = textwrap.TextWrapper()
1277 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1278 return wrapper.fill(self.description)
1279 return self.description
1280
1281 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001282 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001283 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001284 self.patchset = self._GitGetBranchConfigValue(
1285 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001286 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 return self.patchset
1288
1289 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001290 """Set this branch's patchset. If patchset=0, clears the patchset."""
1291 assert self.GetBranch()
1292 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001293 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001294 else:
1295 self.patchset = int(patchset)
1296 self._GitSetBranchConfigValue(
1297 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001299 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001300 """Set this branch's issue. If issue isn't given, clears the issue."""
1301 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001303 issue = int(issue)
1304 self._GitSetBranchConfigValue(
1305 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001306 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001307 codereview_server = self._codereview_impl.GetCodereviewServer()
1308 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001309 self._GitSetBranchConfigValue(
1310 self._codereview_impl.CodereviewServerConfigKey(),
1311 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 else:
tandrii5d48c322016-08-18 16:19:37 -07001313 # Reset all of these just to be clean.
1314 reset_suffixes = [
1315 'last-upload-hash',
1316 self._codereview_impl.IssueConfigKey(),
1317 self._codereview_impl.PatchsetConfigKey(),
1318 self._codereview_impl.CodereviewServerConfigKey(),
1319 ] + self._PostUnsetIssueProperties()
1320 for prop in reset_suffixes:
1321 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001322 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001323 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001325 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 if not self.GitSanityChecks(upstream_branch):
1327 DieWithError('\nGit sanity check failure')
1328
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001329 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001330 if not root:
1331 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001332 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001333
1334 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001335 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001336 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001337 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001338 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001339 except subprocess2.CalledProcessError:
1340 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001341 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001342 'This branch probably doesn\'t exist anymore. To reset the\n'
1343 'tracking branch, please run\n'
1344 ' git branch --set-upstream %s trunk\n'
1345 'replacing trunk with origin/master or the relevant branch') %
1346 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001347
maruel@chromium.org52424302012-08-29 15:14:30 +00001348 issue = self.GetIssue()
1349 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001350 if issue:
1351 description = self.GetDescription()
1352 else:
1353 # If the change was never uploaded, use the log messages of all commits
1354 # up to the branch point, as git cl upload will prefill the description
1355 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001356 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1357 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001358
1359 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001360 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001361 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001362 name,
1363 description,
1364 absroot,
1365 files,
1366 issue,
1367 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001368 author,
1369 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001370
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001371 def UpdateDescription(self, description):
1372 self.description = description
1373 return self._codereview_impl.UpdateDescriptionRemote(description)
1374
1375 def RunHook(self, committing, may_prompt, verbose, change):
1376 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1377 try:
1378 return presubmit_support.DoPresubmitChecks(change, committing,
1379 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1380 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001381 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1382 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001383 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001384 DieWithError(
1385 ('%s\nMaybe your depot_tools is out of date?\n'
1386 'If all fails, contact maruel@') % e)
1387
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001388 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1389 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001390 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1391 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001392 else:
1393 # Assume url.
1394 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1395 urlparse.urlparse(issue_arg))
1396 if not parsed_issue_arg or not parsed_issue_arg.valid:
1397 DieWithError('Failed to parse issue argument "%s". '
1398 'Must be an issue number or a valid URL.' % issue_arg)
1399 return self._codereview_impl.CMDPatchWithParsedIssue(
1400 parsed_issue_arg, reject, nocommit, directory)
1401
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001402 def CMDUpload(self, options, git_diff_args, orig_args):
1403 """Uploads a change to codereview."""
1404 if git_diff_args:
1405 # TODO(ukai): is it ok for gerrit case?
1406 base_branch = git_diff_args[0]
1407 else:
1408 if self.GetBranch() is None:
1409 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1410
1411 # Default to diffing against common ancestor of upstream branch
1412 base_branch = self.GetCommonAncestorWithUpstream()
1413 git_diff_args = [base_branch, 'HEAD']
1414
1415 # Make sure authenticated to codereview before running potentially expensive
1416 # hooks. It is a fast, best efforts check. Codereview still can reject the
1417 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001418 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001419
1420 # Apply watchlists on upload.
1421 change = self.GetChange(base_branch, None)
1422 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1423 files = [f.LocalPath() for f in change.AffectedFiles()]
1424 if not options.bypass_watchlists:
1425 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1426
1427 if not options.bypass_hooks:
1428 if options.reviewers or options.tbr_owners:
1429 # Set the reviewer list now so that presubmit checks can access it.
1430 change_description = ChangeDescription(change.FullDescriptionText())
1431 change_description.update_reviewers(options.reviewers,
1432 options.tbr_owners,
1433 change)
1434 change.SetDescriptionText(change_description.description)
1435 hook_results = self.RunHook(committing=False,
1436 may_prompt=not options.force,
1437 verbose=options.verbose,
1438 change=change)
1439 if not hook_results.should_continue():
1440 return 1
1441 if not options.reviewers and hook_results.reviewers:
1442 options.reviewers = hook_results.reviewers.split(',')
1443
1444 if self.GetIssue():
1445 latest_patchset = self.GetMostRecentPatchset()
1446 local_patchset = self.GetPatchset()
1447 if (latest_patchset and local_patchset and
1448 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001449 print('The last upload made from this repository was patchset #%d but '
1450 'the most recent patchset on the server is #%d.'
1451 % (local_patchset, latest_patchset))
1452 print('Uploading will still work, but if you\'ve uploaded to this '
1453 'issue from another machine or branch the patch you\'re '
1454 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001455 ask_for_data('About to upload; enter to confirm.')
1456
1457 print_stats(options.similarity, options.find_copies, git_diff_args)
1458 ret = self.CMDUploadChange(options, git_diff_args, change)
1459 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001460 if options.use_commit_queue:
1461 self.SetCQState(_CQState.COMMIT)
1462 elif options.cq_dry_run:
1463 self.SetCQState(_CQState.DRY_RUN)
1464
tandrii5d48c322016-08-18 16:19:37 -07001465 _git_set_branch_config_value('last-upload-hash',
1466 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001467 # Run post upload hooks, if specified.
1468 if settings.GetRunPostUploadHook():
1469 presubmit_support.DoPostUploadExecuter(
1470 change,
1471 self,
1472 settings.GetRoot(),
1473 options.verbose,
1474 sys.stdout)
1475
1476 # Upload all dependencies if specified.
1477 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001478 print()
1479 print('--dependencies has been specified.')
1480 print('All dependent local branches will be re-uploaded.')
1481 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001482 # Remove the dependencies flag from args so that we do not end up in a
1483 # loop.
1484 orig_args.remove('--dependencies')
1485 ret = upload_branch_deps(self, orig_args)
1486 return ret
1487
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001488 def SetCQState(self, new_state):
1489 """Update the CQ state for latest patchset.
1490
1491 Issue must have been already uploaded and known.
1492 """
1493 assert new_state in _CQState.ALL_STATES
1494 assert self.GetIssue()
1495 return self._codereview_impl.SetCQState(new_state)
1496
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001497 # Forward methods to codereview specific implementation.
1498
1499 def CloseIssue(self):
1500 return self._codereview_impl.CloseIssue()
1501
1502 def GetStatus(self):
1503 return self._codereview_impl.GetStatus()
1504
1505 def GetCodereviewServer(self):
1506 return self._codereview_impl.GetCodereviewServer()
1507
1508 def GetApprovingReviewers(self):
1509 return self._codereview_impl.GetApprovingReviewers()
1510
1511 def GetMostRecentPatchset(self):
1512 return self._codereview_impl.GetMostRecentPatchset()
1513
1514 def __getattr__(self, attr):
1515 # This is because lots of untested code accesses Rietveld-specific stuff
1516 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001517 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001518 # Note that child method defines __getattr__ as well, and forwards it here,
1519 # because _RietveldChangelistImpl is not cleaned up yet, and given
1520 # deprecation of Rietveld, it should probably be just removed.
1521 # Until that time, avoid infinite recursion by bypassing __getattr__
1522 # of implementation class.
1523 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001524
1525
1526class _ChangelistCodereviewBase(object):
1527 """Abstract base class encapsulating codereview specifics of a changelist."""
1528 def __init__(self, changelist):
1529 self._changelist = changelist # instance of Changelist
1530
1531 def __getattr__(self, attr):
1532 # Forward methods to changelist.
1533 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1534 # _RietveldChangelistImpl to avoid this hack?
1535 return getattr(self._changelist, attr)
1536
1537 def GetStatus(self):
1538 """Apply a rough heuristic to give a simple summary of an issue's review
1539 or CQ status, assuming adherence to a common workflow.
1540
1541 Returns None if no issue for this branch, or specific string keywords.
1542 """
1543 raise NotImplementedError()
1544
1545 def GetCodereviewServer(self):
1546 """Returns server URL without end slash, like "https://codereview.com"."""
1547 raise NotImplementedError()
1548
1549 def FetchDescription(self):
1550 """Fetches and returns description from the codereview server."""
1551 raise NotImplementedError()
1552
tandrii5d48c322016-08-18 16:19:37 -07001553 @classmethod
1554 def IssueConfigKey(cls):
1555 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001556 raise NotImplementedError()
1557
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001558 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001559 def PatchsetConfigKey(cls):
1560 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001561 raise NotImplementedError()
1562
tandrii5d48c322016-08-18 16:19:37 -07001563 @classmethod
1564 def CodereviewServerConfigKey(cls):
1565 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001566 raise NotImplementedError()
1567
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001568 def _PostUnsetIssueProperties(self):
1569 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001570 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001571
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001572 def GetRieveldObjForPresubmit(self):
1573 # This is an unfortunate Rietveld-embeddedness in presubmit.
1574 # For non-Rietveld codereviews, this probably should return a dummy object.
1575 raise NotImplementedError()
1576
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001577 def GetGerritObjForPresubmit(self):
1578 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1579 return None
1580
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001581 def UpdateDescriptionRemote(self, description):
1582 """Update the description on codereview site."""
1583 raise NotImplementedError()
1584
1585 def CloseIssue(self):
1586 """Closes the issue."""
1587 raise NotImplementedError()
1588
1589 def GetApprovingReviewers(self):
1590 """Returns a list of reviewers approving the change.
1591
1592 Note: not necessarily committers.
1593 """
1594 raise NotImplementedError()
1595
1596 def GetMostRecentPatchset(self):
1597 """Returns the most recent patchset number from the codereview site."""
1598 raise NotImplementedError()
1599
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001600 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1601 directory):
1602 """Fetches and applies the issue.
1603
1604 Arguments:
1605 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1606 reject: if True, reject the failed patch instead of switching to 3-way
1607 merge. Rietveld only.
1608 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1609 only.
1610 directory: switch to directory before applying the patch. Rietveld only.
1611 """
1612 raise NotImplementedError()
1613
1614 @staticmethod
1615 def ParseIssueURL(parsed_url):
1616 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1617 failed."""
1618 raise NotImplementedError()
1619
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001620 def EnsureAuthenticated(self, force):
1621 """Best effort check that user is authenticated with codereview server.
1622
1623 Arguments:
1624 force: whether to skip confirmation questions.
1625 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001626 raise NotImplementedError()
1627
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001628 def CMDUploadChange(self, options, args, change):
1629 """Uploads a change to codereview."""
1630 raise NotImplementedError()
1631
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001632 def SetCQState(self, new_state):
1633 """Update the CQ state for latest patchset.
1634
1635 Issue must have been already uploaded and known.
1636 """
1637 raise NotImplementedError()
1638
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001639
1640class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1641 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1642 super(_RietveldChangelistImpl, self).__init__(changelist)
1643 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001644 if not rietveld_server:
1645 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001646
1647 self._rietveld_server = rietveld_server
1648 self._auth_config = auth_config
1649 self._props = None
1650 self._rpc_server = None
1651
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001652 def GetCodereviewServer(self):
1653 if not self._rietveld_server:
1654 # If we're on a branch then get the server potentially associated
1655 # with that branch.
1656 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001657 self._rietveld_server = gclient_utils.UpgradeToHttps(
1658 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001659 if not self._rietveld_server:
1660 self._rietveld_server = settings.GetDefaultServerUrl()
1661 return self._rietveld_server
1662
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001663 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001664 """Best effort check that user is authenticated with Rietveld server."""
1665 if self._auth_config.use_oauth2:
1666 authenticator = auth.get_authenticator_for_host(
1667 self.GetCodereviewServer(), self._auth_config)
1668 if not authenticator.has_cached_credentials():
1669 raise auth.LoginRequiredError(self.GetCodereviewServer())
1670
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001671 def FetchDescription(self):
1672 issue = self.GetIssue()
1673 assert issue
1674 try:
1675 return self.RpcServer().get_description(issue).strip()
1676 except urllib2.HTTPError as e:
1677 if e.code == 404:
1678 DieWithError(
1679 ('\nWhile fetching the description for issue %d, received a '
1680 '404 (not found)\n'
1681 'error. It is likely that you deleted this '
1682 'issue on the server. If this is the\n'
1683 'case, please run\n\n'
1684 ' git cl issue 0\n\n'
1685 'to clear the association with the deleted issue. Then run '
1686 'this command again.') % issue)
1687 else:
1688 DieWithError(
1689 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1690 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001691 print('Warning: Failed to retrieve CL description due to network '
1692 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001693 return ''
1694
1695 def GetMostRecentPatchset(self):
1696 return self.GetIssueProperties()['patchsets'][-1]
1697
1698 def GetPatchSetDiff(self, issue, patchset):
1699 return self.RpcServer().get(
1700 '/download/issue%s_%s.diff' % (issue, patchset))
1701
1702 def GetIssueProperties(self):
1703 if self._props is None:
1704 issue = self.GetIssue()
1705 if not issue:
1706 self._props = {}
1707 else:
1708 self._props = self.RpcServer().get_issue_properties(issue, True)
1709 return self._props
1710
1711 def GetApprovingReviewers(self):
1712 return get_approving_reviewers(self.GetIssueProperties())
1713
1714 def AddComment(self, message):
1715 return self.RpcServer().add_comment(self.GetIssue(), message)
1716
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001717 def GetStatus(self):
1718 """Apply a rough heuristic to give a simple summary of an issue's review
1719 or CQ status, assuming adherence to a common workflow.
1720
1721 Returns None if no issue for this branch, or one of the following keywords:
1722 * 'error' - error from review tool (including deleted issues)
1723 * 'unsent' - not sent for review
1724 * 'waiting' - waiting for review
1725 * 'reply' - waiting for owner to reply to review
1726 * 'lgtm' - LGTM from at least one approved reviewer
1727 * 'commit' - in the commit queue
1728 * 'closed' - closed
1729 """
1730 if not self.GetIssue():
1731 return None
1732
1733 try:
1734 props = self.GetIssueProperties()
1735 except urllib2.HTTPError:
1736 return 'error'
1737
1738 if props.get('closed'):
1739 # Issue is closed.
1740 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001741 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001742 # Issue is in the commit queue.
1743 return 'commit'
1744
1745 try:
1746 reviewers = self.GetApprovingReviewers()
1747 except urllib2.HTTPError:
1748 return 'error'
1749
1750 if reviewers:
1751 # Was LGTM'ed.
1752 return 'lgtm'
1753
1754 messages = props.get('messages') or []
1755
tandrii9d2c7a32016-06-22 03:42:45 -07001756 # Skip CQ messages that don't require owner's action.
1757 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1758 if 'Dry run:' in messages[-1]['text']:
1759 messages.pop()
1760 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1761 # This message always follows prior messages from CQ,
1762 # so skip this too.
1763 messages.pop()
1764 else:
1765 # This is probably a CQ messages warranting user attention.
1766 break
1767
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001768 if not messages:
1769 # No message was sent.
1770 return 'unsent'
1771 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001772 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001773 return 'reply'
1774 return 'waiting'
1775
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001777 return self.RpcServer().update_description(
1778 self.GetIssue(), self.description)
1779
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001780 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001781 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001783 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001784 return self.SetFlags({flag: value})
1785
1786 def SetFlags(self, flags):
1787 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001788 """
phajdan.jr68598232016-08-10 03:28:28 -07001789 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001790 try:
tandrii4b233bd2016-07-06 03:50:29 -07001791 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001792 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001793 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001794 if e.code == 404:
1795 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1796 if e.code == 403:
1797 DieWithError(
1798 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001799 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001800 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001802 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803 """Returns an upload.RpcServer() to access this review's rietveld instance.
1804 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001805 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001806 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001808 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001809 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001811 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001812 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001813 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814
tandrii5d48c322016-08-18 16:19:37 -07001815 @classmethod
1816 def PatchsetConfigKey(cls):
1817 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818
tandrii5d48c322016-08-18 16:19:37 -07001819 @classmethod
1820 def CodereviewServerConfigKey(cls):
1821 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001822
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001823 def GetRieveldObjForPresubmit(self):
1824 return self.RpcServer()
1825
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001826 def SetCQState(self, new_state):
1827 props = self.GetIssueProperties()
1828 if props.get('private'):
1829 DieWithError('Cannot set-commit on private issue')
1830
1831 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001832 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001833 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001834 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001835 else:
tandrii4b233bd2016-07-06 03:50:29 -07001836 assert new_state == _CQState.DRY_RUN
1837 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001838
1839
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001840 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1841 directory):
1842 # TODO(maruel): Use apply_issue.py
1843
1844 # PatchIssue should never be called with a dirty tree. It is up to the
1845 # caller to check this, but just in case we assert here since the
1846 # consequences of the caller not checking this could be dire.
1847 assert(not git_common.is_dirty_git_tree('apply'))
1848 assert(parsed_issue_arg.valid)
1849 self._changelist.issue = parsed_issue_arg.issue
1850 if parsed_issue_arg.hostname:
1851 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1852
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001853 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1854 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001855 assert parsed_issue_arg.patchset
1856 patchset = parsed_issue_arg.patchset
1857 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1858 else:
1859 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1860 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1861
1862 # Switch up to the top-level directory, if necessary, in preparation for
1863 # applying the patch.
1864 top = settings.GetRelativeRoot()
1865 if top:
1866 os.chdir(top)
1867
1868 # Git patches have a/ at the beginning of source paths. We strip that out
1869 # with a sed script rather than the -p flag to patch so we can feed either
1870 # Git or svn-style patches into the same apply command.
1871 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1872 try:
1873 patch_data = subprocess2.check_output(
1874 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1875 except subprocess2.CalledProcessError:
1876 DieWithError('Git patch mungling failed.')
1877 logging.info(patch_data)
1878
1879 # We use "git apply" to apply the patch instead of "patch" so that we can
1880 # pick up file adds.
1881 # The --index flag means: also insert into the index (so we catch adds).
1882 cmd = ['git', 'apply', '--index', '-p0']
1883 if directory:
1884 cmd.extend(('--directory', directory))
1885 if reject:
1886 cmd.append('--reject')
1887 elif IsGitVersionAtLeast('1.7.12'):
1888 cmd.append('--3way')
1889 try:
1890 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1891 stdin=patch_data, stdout=subprocess2.VOID)
1892 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001893 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001894 return 1
1895
1896 # If we had an issue, commit the current state and register the issue.
1897 if not nocommit:
1898 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1899 'patch from issue %(i)s at patchset '
1900 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1901 % {'i': self.GetIssue(), 'p': patchset})])
1902 self.SetIssue(self.GetIssue())
1903 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001904 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001905 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001906 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001907 return 0
1908
1909 @staticmethod
1910 def ParseIssueURL(parsed_url):
1911 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1912 return None
wychen3c1c1722016-08-04 11:46:36 -07001913 # Rietveld patch: https://domain/<number>/#ps<patchset>
1914 match = re.match(r'/(\d+)/$', parsed_url.path)
1915 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1916 if match and match2:
1917 return _RietveldParsedIssueNumberArgument(
1918 issue=int(match.group(1)),
1919 patchset=int(match2.group(1)),
1920 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001921 # Typical url: https://domain/<issue_number>[/[other]]
1922 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1923 if match:
1924 return _RietveldParsedIssueNumberArgument(
1925 issue=int(match.group(1)),
1926 hostname=parsed_url.netloc)
1927 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1928 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1929 if match:
1930 return _RietveldParsedIssueNumberArgument(
1931 issue=int(match.group(1)),
1932 patchset=int(match.group(2)),
1933 hostname=parsed_url.netloc,
1934 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1935 return None
1936
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001937 def CMDUploadChange(self, options, args, change):
1938 """Upload the patch to Rietveld."""
1939 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1940 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001941 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1942 if options.emulate_svn_auto_props:
1943 upload_args.append('--emulate_svn_auto_props')
1944
1945 change_desc = None
1946
1947 if options.email is not None:
1948 upload_args.extend(['--email', options.email])
1949
1950 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001951 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001952 upload_args.extend(['--title', options.title])
1953 if options.message:
1954 upload_args.extend(['--message', options.message])
1955 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001956 print('This branch is associated with issue %s. '
1957 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001958 else:
nodirca166002016-06-27 10:59:51 -07001959 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001960 upload_args.extend(['--title', options.title])
1961 message = (options.title or options.message or
1962 CreateDescriptionFromLog(args))
1963 change_desc = ChangeDescription(message)
1964 if options.reviewers or options.tbr_owners:
1965 change_desc.update_reviewers(options.reviewers,
1966 options.tbr_owners,
1967 change)
1968 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001969 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001970
1971 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001972 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001973 return 1
1974
1975 upload_args.extend(['--message', change_desc.description])
1976 if change_desc.get_reviewers():
1977 upload_args.append('--reviewers=%s' % ','.join(
1978 change_desc.get_reviewers()))
1979 if options.send_mail:
1980 if not change_desc.get_reviewers():
1981 DieWithError("Must specify reviewers to send email.")
1982 upload_args.append('--send_mail')
1983
1984 # We check this before applying rietveld.private assuming that in
1985 # rietveld.cc only addresses which we can send private CLs to are listed
1986 # if rietveld.private is set, and so we should ignore rietveld.cc only
1987 # when --private is specified explicitly on the command line.
1988 if options.private:
1989 logging.warn('rietveld.cc is ignored since private flag is specified. '
1990 'You need to review and add them manually if necessary.')
1991 cc = self.GetCCListWithoutDefault()
1992 else:
1993 cc = self.GetCCList()
1994 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1995 if cc:
1996 upload_args.extend(['--cc', cc])
1997
1998 if options.private or settings.GetDefaultPrivateFlag() == "True":
1999 upload_args.append('--private')
2000
2001 upload_args.extend(['--git_similarity', str(options.similarity)])
2002 if not options.find_copies:
2003 upload_args.extend(['--git_no_find_copies'])
2004
2005 # Include the upstream repo's URL in the change -- this is useful for
2006 # projects that have their source spread across multiple repos.
2007 remote_url = self.GetGitBaseUrlFromConfig()
2008 if not remote_url:
2009 if settings.GetIsGitSvn():
2010 remote_url = self.GetGitSvnRemoteUrl()
2011 else:
2012 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2013 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2014 self.GetUpstreamBranch().split('/')[-1])
2015 if remote_url:
2016 upload_args.extend(['--base_url', remote_url])
2017 remote, remote_branch = self.GetRemoteBranch()
2018 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2019 settings.GetPendingRefPrefix())
2020 if target_ref:
2021 upload_args.extend(['--target_ref', target_ref])
2022
2023 # Look for dependent patchsets. See crbug.com/480453 for more details.
2024 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2025 upstream_branch = ShortBranchName(upstream_branch)
2026 if remote is '.':
2027 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002028 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002029 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002030 print()
2031 print('Skipping dependency patchset upload because git config '
2032 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2033 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002034 else:
2035 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002036 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002037 auth_config=auth_config)
2038 branch_cl_issue_url = branch_cl.GetIssueURL()
2039 branch_cl_issue = branch_cl.GetIssue()
2040 branch_cl_patchset = branch_cl.GetPatchset()
2041 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2042 upload_args.extend(
2043 ['--depends_on_patchset', '%s:%s' % (
2044 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002045 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002046 '\n'
2047 'The current branch (%s) is tracking a local branch (%s) with '
2048 'an associated CL.\n'
2049 'Adding %s/#ps%s as a dependency patchset.\n'
2050 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2051 branch_cl_patchset))
2052
2053 project = settings.GetProject()
2054 if project:
2055 upload_args.extend(['--project', project])
2056
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002057 try:
2058 upload_args = ['upload'] + upload_args + args
2059 logging.info('upload.RealMain(%s)', upload_args)
2060 issue, patchset = upload.RealMain(upload_args)
2061 issue = int(issue)
2062 patchset = int(patchset)
2063 except KeyboardInterrupt:
2064 sys.exit(1)
2065 except:
2066 # If we got an exception after the user typed a description for their
2067 # change, back up the description before re-raising.
2068 if change_desc:
2069 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2070 print('\nGot exception while uploading -- saving description to %s\n' %
2071 backup_path)
2072 backup_file = open(backup_path, 'w')
2073 backup_file.write(change_desc.description)
2074 backup_file.close()
2075 raise
2076
2077 if not self.GetIssue():
2078 self.SetIssue(issue)
2079 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002080 return 0
2081
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002082
2083class _GerritChangelistImpl(_ChangelistCodereviewBase):
2084 def __init__(self, changelist, auth_config=None):
2085 # auth_config is Rietveld thing, kept here to preserve interface only.
2086 super(_GerritChangelistImpl, self).__init__(changelist)
2087 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002088 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002089 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002090 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002091
2092 def _GetGerritHost(self):
2093 # Lazy load of configs.
2094 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002095 if self._gerrit_host and '.' not in self._gerrit_host:
2096 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2097 # This happens for internal stuff http://crbug.com/614312.
2098 parsed = urlparse.urlparse(self.GetRemoteUrl())
2099 if parsed.scheme == 'sso':
2100 print('WARNING: using non https URLs for remote is likely broken\n'
2101 ' Your current remote is: %s' % self.GetRemoteUrl())
2102 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2103 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002104 return self._gerrit_host
2105
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002106 def _GetGitHost(self):
2107 """Returns git host to be used when uploading change to Gerrit."""
2108 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2109
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002110 def GetCodereviewServer(self):
2111 if not self._gerrit_server:
2112 # If we're on a branch then get the server potentially associated
2113 # with that branch.
2114 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002115 self._gerrit_server = self._GitGetBranchConfigValue(
2116 self.CodereviewServerConfigKey())
2117 if self._gerrit_server:
2118 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002119 if not self._gerrit_server:
2120 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2121 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002122 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002123 parts[0] = parts[0] + '-review'
2124 self._gerrit_host = '.'.join(parts)
2125 self._gerrit_server = 'https://%s' % self._gerrit_host
2126 return self._gerrit_server
2127
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002128 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002129 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002130 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002131
tandrii5d48c322016-08-18 16:19:37 -07002132 @classmethod
2133 def PatchsetConfigKey(cls):
2134 return 'gerritpatchset'
2135
2136 @classmethod
2137 def CodereviewServerConfigKey(cls):
2138 return 'gerritserver'
2139
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002140 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002141 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002142 if settings.GetGerritSkipEnsureAuthenticated():
2143 # For projects with unusual authentication schemes.
2144 # See http://crbug.com/603378.
2145 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002146 # Lazy-loader to identify Gerrit and Git hosts.
2147 if gerrit_util.GceAuthenticator.is_gce():
2148 return
2149 self.GetCodereviewServer()
2150 git_host = self._GetGitHost()
2151 assert self._gerrit_server and self._gerrit_host
2152 cookie_auth = gerrit_util.CookiesAuthenticator()
2153
2154 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2155 git_auth = cookie_auth.get_auth_header(git_host)
2156 if gerrit_auth and git_auth:
2157 if gerrit_auth == git_auth:
2158 return
2159 print((
2160 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2161 ' Check your %s or %s file for credentials of hosts:\n'
2162 ' %s\n'
2163 ' %s\n'
2164 ' %s') %
2165 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2166 git_host, self._gerrit_host,
2167 cookie_auth.get_new_password_message(git_host)))
2168 if not force:
2169 ask_for_data('If you know what you are doing, press Enter to continue, '
2170 'Ctrl+C to abort.')
2171 return
2172 else:
2173 missing = (
2174 [] if gerrit_auth else [self._gerrit_host] +
2175 [] if git_auth else [git_host])
2176 DieWithError('Credentials for the following hosts are required:\n'
2177 ' %s\n'
2178 'These are read from %s (or legacy %s)\n'
2179 '%s' % (
2180 '\n '.join(missing),
2181 cookie_auth.get_gitcookies_path(),
2182 cookie_auth.get_netrc_path(),
2183 cookie_auth.get_new_password_message(git_host)))
2184
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002185 def _PostUnsetIssueProperties(self):
2186 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002187 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002188
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002189 def GetRieveldObjForPresubmit(self):
2190 class ThisIsNotRietveldIssue(object):
2191 def __nonzero__(self):
2192 # This is a hack to make presubmit_support think that rietveld is not
2193 # defined, yet still ensure that calls directly result in a decent
2194 # exception message below.
2195 return False
2196
2197 def __getattr__(self, attr):
2198 print(
2199 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2200 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2201 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2202 'or use Rietveld for codereview.\n'
2203 'See also http://crbug.com/579160.' % attr)
2204 raise NotImplementedError()
2205 return ThisIsNotRietveldIssue()
2206
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002207 def GetGerritObjForPresubmit(self):
2208 return presubmit_support.GerritAccessor(self._GetGerritHost())
2209
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002210 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002211 """Apply a rough heuristic to give a simple summary of an issue's review
2212 or CQ status, assuming adherence to a common workflow.
2213
2214 Returns None if no issue for this branch, or one of the following keywords:
2215 * 'error' - error from review tool (including deleted issues)
2216 * 'unsent' - no reviewers added
2217 * 'waiting' - waiting for review
2218 * 'reply' - waiting for owner to reply to review
2219 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2220 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2221 * 'commit' - in the commit queue
2222 * 'closed' - abandoned
2223 """
2224 if not self.GetIssue():
2225 return None
2226
2227 try:
2228 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2229 except httplib.HTTPException:
2230 return 'error'
2231
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002232 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002233 return 'closed'
2234
2235 cq_label = data['labels'].get('Commit-Queue', {})
2236 if cq_label:
2237 # Vote value is a stringified integer, which we expect from 0 to 2.
2238 vote_value = cq_label.get('value', '0')
2239 vote_text = cq_label.get('values', {}).get(vote_value, '')
2240 if vote_text.lower() == 'commit':
2241 return 'commit'
2242
2243 lgtm_label = data['labels'].get('Code-Review', {})
2244 if lgtm_label:
2245 if 'rejected' in lgtm_label:
2246 return 'not lgtm'
2247 if 'approved' in lgtm_label:
2248 return 'lgtm'
2249
2250 if not data.get('reviewers', {}).get('REVIEWER', []):
2251 return 'unsent'
2252
2253 messages = data.get('messages', [])
2254 if messages:
2255 owner = data['owner'].get('_account_id')
2256 last_message_author = messages[-1].get('author', {}).get('_account_id')
2257 if owner != last_message_author:
2258 # Some reply from non-owner.
2259 return 'reply'
2260
2261 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002262
2263 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002264 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002265 return data['revisions'][data['current_revision']]['_number']
2266
2267 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002268 data = self._GetChangeDetail(['CURRENT_REVISION'])
2269 current_rev = data['current_revision']
2270 url = data['revisions'][current_rev]['fetch']['http']['url']
2271 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002272
2273 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002274 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2275 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002276
2277 def CloseIssue(self):
2278 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2279
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002280 def GetApprovingReviewers(self):
2281 """Returns a list of reviewers approving the change.
2282
2283 Note: not necessarily committers.
2284 """
2285 raise NotImplementedError()
2286
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002287 def SubmitIssue(self, wait_for_merge=True):
2288 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2289 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002290
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002291 def _GetChangeDetail(self, options=None, issue=None):
2292 options = options or []
2293 issue = issue or self.GetIssue()
2294 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002295 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2296 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002297
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002298 def CMDLand(self, force, bypass_hooks, verbose):
2299 if git_common.is_dirty_git_tree('land'):
2300 return 1
tandriid60367b2016-06-22 05:25:12 -07002301 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2302 if u'Commit-Queue' in detail.get('labels', {}):
2303 if not force:
2304 ask_for_data('\nIt seems this repository has a Commit Queue, '
2305 'which can test and land changes for you. '
2306 'Are you sure you wish to bypass it?\n'
2307 'Press Enter to continue, Ctrl+C to abort.')
2308
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002309 differs = True
tandrii5d48c322016-08-18 16:19:37 -07002310 last_upload = RunGit(['config', self._GitBranchSetting('gerritsquashhash')],
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002311 error_ok=True).strip()
2312 # Note: git diff outputs nothing if there is no diff.
2313 if not last_upload or RunGit(['diff', last_upload]).strip():
2314 print('WARNING: some changes from local branch haven\'t been uploaded')
2315 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002316 if detail['current_revision'] == last_upload:
2317 differs = False
2318 else:
2319 print('WARNING: local branch contents differ from latest uploaded '
2320 'patchset')
2321 if differs:
2322 if not force:
2323 ask_for_data(
2324 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2325 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2326 elif not bypass_hooks:
2327 hook_results = self.RunHook(
2328 committing=True,
2329 may_prompt=not force,
2330 verbose=verbose,
2331 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2332 if not hook_results.should_continue():
2333 return 1
2334
2335 self.SubmitIssue(wait_for_merge=True)
2336 print('Issue %s has been submitted.' % self.GetIssueURL())
2337 return 0
2338
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002339 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2340 directory):
2341 assert not reject
2342 assert not nocommit
2343 assert not directory
2344 assert parsed_issue_arg.valid
2345
2346 self._changelist.issue = parsed_issue_arg.issue
2347
2348 if parsed_issue_arg.hostname:
2349 self._gerrit_host = parsed_issue_arg.hostname
2350 self._gerrit_server = 'https://%s' % self._gerrit_host
2351
2352 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2353
2354 if not parsed_issue_arg.patchset:
2355 # Use current revision by default.
2356 revision_info = detail['revisions'][detail['current_revision']]
2357 patchset = int(revision_info['_number'])
2358 else:
2359 patchset = parsed_issue_arg.patchset
2360 for revision_info in detail['revisions'].itervalues():
2361 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2362 break
2363 else:
2364 DieWithError('Couldn\'t find patchset %i in issue %i' %
2365 (parsed_issue_arg.patchset, self.GetIssue()))
2366
2367 fetch_info = revision_info['fetch']['http']
2368 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2369 RunGit(['cherry-pick', 'FETCH_HEAD'])
2370 self.SetIssue(self.GetIssue())
2371 self.SetPatchset(patchset)
2372 print('Committed patch for issue %i pathset %i locally' %
2373 (self.GetIssue(), self.GetPatchset()))
2374 return 0
2375
2376 @staticmethod
2377 def ParseIssueURL(parsed_url):
2378 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2379 return None
2380 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2381 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2382 # Short urls like https://domain/<issue_number> can be used, but don't allow
2383 # specifying the patchset (you'd 404), but we allow that here.
2384 if parsed_url.path == '/':
2385 part = parsed_url.fragment
2386 else:
2387 part = parsed_url.path
2388 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2389 if match:
2390 return _ParsedIssueNumberArgument(
2391 issue=int(match.group(2)),
2392 patchset=int(match.group(4)) if match.group(4) else None,
2393 hostname=parsed_url.netloc)
2394 return None
2395
tandrii16e0b4e2016-06-07 10:34:28 -07002396 def _GerritCommitMsgHookCheck(self, offer_removal):
2397 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2398 if not os.path.exists(hook):
2399 return
2400 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2401 # custom developer made one.
2402 data = gclient_utils.FileRead(hook)
2403 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2404 return
2405 print('Warning: you have Gerrit commit-msg hook installed.\n'
2406 'It is not neccessary for uploading with git cl in squash mode, '
2407 'and may interfere with it in subtle ways.\n'
2408 'We recommend you remove the commit-msg hook.')
2409 if offer_removal:
2410 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2411 if reply.lower().startswith('y'):
2412 gclient_utils.rm_file_or_tree(hook)
2413 print('Gerrit commit-msg hook removed.')
2414 else:
2415 print('OK, will keep Gerrit commit-msg hook in place.')
2416
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002417 def CMDUploadChange(self, options, args, change):
2418 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002419 if options.squash and options.no_squash:
2420 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002421
2422 if not options.squash and not options.no_squash:
2423 # Load default for user, repo, squash=true, in this order.
2424 options.squash = settings.GetSquashGerritUploads()
2425 elif options.no_squash:
2426 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002427
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002428 # We assume the remote called "origin" is the one we want.
2429 # It is probably not worthwhile to support different workflows.
2430 gerrit_remote = 'origin'
2431
2432 remote, remote_branch = self.GetRemoteBranch()
2433 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2434 pending_prefix='')
2435
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002436 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002437 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002438 if self.GetIssue():
2439 # Try to get the message from a previous upload.
2440 message = self.GetDescription()
2441 if not message:
2442 DieWithError(
2443 'failed to fetch description from current Gerrit issue %d\n'
2444 '%s' % (self.GetIssue(), self.GetIssueURL()))
2445 change_id = self._GetChangeDetail()['change_id']
2446 while True:
2447 footer_change_ids = git_footers.get_footer_change_id(message)
2448 if footer_change_ids == [change_id]:
2449 break
2450 if not footer_change_ids:
2451 message = git_footers.add_footer_change_id(message, change_id)
2452 print('WARNING: appended missing Change-Id to issue description')
2453 continue
2454 # There is already a valid footer but with different or several ids.
2455 # Doing this automatically is non-trivial as we don't want to lose
2456 # existing other footers, yet we want to append just 1 desired
2457 # Change-Id. Thus, just create a new footer, but let user verify the
2458 # new description.
2459 message = '%s\n\nChange-Id: %s' % (message, change_id)
2460 print(
2461 'WARNING: issue %s has Change-Id footer(s):\n'
2462 ' %s\n'
2463 'but issue has Change-Id %s, according to Gerrit.\n'
2464 'Please, check the proposed correction to the description, '
2465 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2466 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2467 change_id))
2468 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2469 if not options.force:
2470 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002471 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002472 message = change_desc.description
2473 if not message:
2474 DieWithError("Description is empty. Aborting...")
2475 # Continue the while loop.
2476 # Sanity check of this code - we should end up with proper message
2477 # footer.
2478 assert [change_id] == git_footers.get_footer_change_id(message)
2479 change_desc = ChangeDescription(message)
2480 else:
2481 change_desc = ChangeDescription(
2482 options.message or CreateDescriptionFromLog(args))
2483 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002484 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002485 if not change_desc.description:
2486 DieWithError("Description is empty. Aborting...")
2487 message = change_desc.description
2488 change_ids = git_footers.get_footer_change_id(message)
2489 if len(change_ids) > 1:
2490 DieWithError('too many Change-Id footers, at most 1 allowed.')
2491 if not change_ids:
2492 # Generate the Change-Id automatically.
2493 message = git_footers.add_footer_change_id(
2494 message, GenerateGerritChangeId(message))
2495 change_desc.set_description(message)
2496 change_ids = git_footers.get_footer_change_id(message)
2497 assert len(change_ids) == 1
2498 change_id = change_ids[0]
2499
2500 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2501 if remote is '.':
2502 # If our upstream branch is local, we base our squashed commit on its
2503 # squashed version.
2504 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2505 # Check the squashed hash of the parent.
2506 parent = RunGit(['config',
2507 'branch.%s.gerritsquashhash' % upstream_branch_name],
2508 error_ok=True).strip()
2509 # Verify that the upstream branch has been uploaded too, otherwise
2510 # Gerrit will create additional CLs when uploading.
2511 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2512 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002513 DieWithError(
2514 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002515 'Note: maybe you\'ve uploaded it with --no-squash. '
2516 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002517 ' git cl upload --squash\n' % upstream_branch_name)
2518 else:
2519 parent = self.GetCommonAncestorWithUpstream()
2520
2521 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2522 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2523 '-m', message]).strip()
2524 else:
2525 change_desc = ChangeDescription(
2526 options.message or CreateDescriptionFromLog(args))
2527 if not change_desc.description:
2528 DieWithError("Description is empty. Aborting...")
2529
2530 if not git_footers.get_footer_change_id(change_desc.description):
2531 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002532 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2533 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002534 ref_to_push = 'HEAD'
2535 parent = '%s/%s' % (gerrit_remote, branch)
2536 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2537
2538 assert change_desc
2539 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2540 ref_to_push)]).splitlines()
2541 if len(commits) > 1:
2542 print('WARNING: This will upload %d commits. Run the following command '
2543 'to see which commits will be uploaded: ' % len(commits))
2544 print('git log %s..%s' % (parent, ref_to_push))
2545 print('You can also use `git squash-branch` to squash these into a '
2546 'single commit.')
2547 ask_for_data('About to upload; enter to confirm.')
2548
2549 if options.reviewers or options.tbr_owners:
2550 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2551 change)
2552
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002553 # Extra options that can be specified at push time. Doc:
2554 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2555 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002556 if change_desc.get_reviewers(tbr_only=True):
2557 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2558 refspec_opts.append('l=Code-Review+1')
2559
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002560 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002561 if not re.match(r'^[\w ]+$', options.title):
2562 options.title = re.sub(r'[^\w ]', '', options.title)
2563 print('WARNING: Patchset title may only contain alphanumeric chars '
2564 'and spaces. Cleaned up title:\n%s' % options.title)
2565 if not options.force:
2566 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002567 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2568 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002569 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2570
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002571 if options.send_mail:
2572 if not change_desc.get_reviewers():
2573 DieWithError('Must specify reviewers to send email.')
2574 refspec_opts.append('notify=ALL')
2575 else:
2576 refspec_opts.append('notify=NONE')
2577
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002578 cc = self.GetCCList().split(',')
2579 if options.cc:
2580 cc.extend(options.cc)
2581 cc = filter(None, cc)
2582 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002583 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002584
tandrii99a72f22016-08-17 14:33:24 -07002585 reviewers = change_desc.get_reviewers()
2586 if reviewers:
2587 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002588
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002589 refspec_suffix = ''
2590 if refspec_opts:
2591 refspec_suffix = '%' + ','.join(refspec_opts)
2592 assert ' ' not in refspec_suffix, (
2593 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002594 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002595
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002596 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002597 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002598 print_stdout=True,
2599 # Flush after every line: useful for seeing progress when running as
2600 # recipe.
2601 filter_fn=lambda _: sys.stdout.flush())
2602
2603 if options.squash:
2604 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2605 change_numbers = [m.group(1)
2606 for m in map(regex.match, push_stdout.splitlines())
2607 if m]
2608 if len(change_numbers) != 1:
2609 DieWithError(
2610 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2611 'Change-Id: %s') % (len(change_numbers), change_id))
2612 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002613 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002614 return 0
2615
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002616 def _AddChangeIdToCommitMessage(self, options, args):
2617 """Re-commits using the current message, assumes the commit hook is in
2618 place.
2619 """
2620 log_desc = options.message or CreateDescriptionFromLog(args)
2621 git_command = ['commit', '--amend', '-m', log_desc]
2622 RunGit(git_command)
2623 new_log_desc = CreateDescriptionFromLog(args)
2624 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002625 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002626 return new_log_desc
2627 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002628 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002629
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002630 def SetCQState(self, new_state):
2631 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002632 vote_map = {
2633 _CQState.NONE: 0,
2634 _CQState.DRY_RUN: 1,
2635 _CQState.COMMIT : 2,
2636 }
2637 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2638 labels={'Commit-Queue': vote_map[new_state]})
2639
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002640
2641_CODEREVIEW_IMPLEMENTATIONS = {
2642 'rietveld': _RietveldChangelistImpl,
2643 'gerrit': _GerritChangelistImpl,
2644}
2645
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002646
iannuccie53c9352016-08-17 14:40:40 -07002647def _add_codereview_issue_select_options(parser, extra=""):
2648 _add_codereview_select_options(parser)
2649
2650 text = ('Operate on this issue number instead of the current branch\'s '
2651 'implicit issue.')
2652 if extra:
2653 text += ' '+extra
2654 parser.add_option('-i', '--issue', type=int, help=text)
2655
2656
2657def _process_codereview_issue_select_options(parser, options):
2658 _process_codereview_select_options(parser, options)
2659 if options.issue is not None and not options.forced_codereview:
2660 parser.error('--issue must be specified with either --rietveld or --gerrit')
2661
2662
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002663def _add_codereview_select_options(parser):
2664 """Appends --gerrit and --rietveld options to force specific codereview."""
2665 parser.codereview_group = optparse.OptionGroup(
2666 parser, 'EXPERIMENTAL! Codereview override options')
2667 parser.add_option_group(parser.codereview_group)
2668 parser.codereview_group.add_option(
2669 '--gerrit', action='store_true',
2670 help='Force the use of Gerrit for codereview')
2671 parser.codereview_group.add_option(
2672 '--rietveld', action='store_true',
2673 help='Force the use of Rietveld for codereview')
2674
2675
2676def _process_codereview_select_options(parser, options):
2677 if options.gerrit and options.rietveld:
2678 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2679 options.forced_codereview = None
2680 if options.gerrit:
2681 options.forced_codereview = 'gerrit'
2682 elif options.rietveld:
2683 options.forced_codereview = 'rietveld'
2684
2685
tandriif9aefb72016-07-01 09:06:51 -07002686def _get_bug_line_values(default_project, bugs):
2687 """Given default_project and comma separated list of bugs, yields bug line
2688 values.
2689
2690 Each bug can be either:
2691 * a number, which is combined with default_project
2692 * string, which is left as is.
2693
2694 This function may produce more than one line, because bugdroid expects one
2695 project per line.
2696
2697 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2698 ['v8:123', 'chromium:789']
2699 """
2700 default_bugs = []
2701 others = []
2702 for bug in bugs.split(','):
2703 bug = bug.strip()
2704 if bug:
2705 try:
2706 default_bugs.append(int(bug))
2707 except ValueError:
2708 others.append(bug)
2709
2710 if default_bugs:
2711 default_bugs = ','.join(map(str, default_bugs))
2712 if default_project:
2713 yield '%s:%s' % (default_project, default_bugs)
2714 else:
2715 yield default_bugs
2716 for other in sorted(others):
2717 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2718 yield other
2719
2720
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002721class ChangeDescription(object):
2722 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002723 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002724 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002725
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002726 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002727 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002728
agable@chromium.org42c20792013-09-12 17:34:49 +00002729 @property # www.logilab.org/ticket/89786
2730 def description(self): # pylint: disable=E0202
2731 return '\n'.join(self._description_lines)
2732
2733 def set_description(self, desc):
2734 if isinstance(desc, basestring):
2735 lines = desc.splitlines()
2736 else:
2737 lines = [line.rstrip() for line in desc]
2738 while lines and not lines[0]:
2739 lines.pop(0)
2740 while lines and not lines[-1]:
2741 lines.pop(-1)
2742 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002743
piman@chromium.org336f9122014-09-04 02:16:55 +00002744 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002745 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002746 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002747 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002748 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002749 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002750
agable@chromium.org42c20792013-09-12 17:34:49 +00002751 # Get the set of R= and TBR= lines and remove them from the desciption.
2752 regexp = re.compile(self.R_LINE)
2753 matches = [regexp.match(line) for line in self._description_lines]
2754 new_desc = [l for i, l in enumerate(self._description_lines)
2755 if not matches[i]]
2756 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002757
agable@chromium.org42c20792013-09-12 17:34:49 +00002758 # Construct new unified R= and TBR= lines.
2759 r_names = []
2760 tbr_names = []
2761 for match in matches:
2762 if not match:
2763 continue
2764 people = cleanup_list([match.group(2).strip()])
2765 if match.group(1) == 'TBR':
2766 tbr_names.extend(people)
2767 else:
2768 r_names.extend(people)
2769 for name in r_names:
2770 if name not in reviewers:
2771 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002772 if add_owners_tbr:
2773 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002774 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002775 all_reviewers = set(tbr_names + reviewers)
2776 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2777 all_reviewers)
2778 tbr_names.extend(owners_db.reviewers_for(missing_files,
2779 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002780 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2781 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2782
2783 # Put the new lines in the description where the old first R= line was.
2784 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2785 if 0 <= line_loc < len(self._description_lines):
2786 if new_tbr_line:
2787 self._description_lines.insert(line_loc, new_tbr_line)
2788 if new_r_line:
2789 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002790 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002791 if new_r_line:
2792 self.append_footer(new_r_line)
2793 if new_tbr_line:
2794 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002795
tandriif9aefb72016-07-01 09:06:51 -07002796 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002797 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002798 self.set_description([
2799 '# Enter a description of the change.',
2800 '# This will be displayed on the codereview site.',
2801 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002802 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002803 '--------------------',
2804 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002805
agable@chromium.org42c20792013-09-12 17:34:49 +00002806 regexp = re.compile(self.BUG_LINE)
2807 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002808 prefix = settings.GetBugPrefix()
2809 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2810 for value in values:
2811 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2812 self.append_footer('BUG=%s' % value)
2813
agable@chromium.org42c20792013-09-12 17:34:49 +00002814 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002815 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002816 if not content:
2817 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002818 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002819
2820 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002821 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2822 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002823 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002824 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002825
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002826 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002827 """Adds a footer line to the description.
2828
2829 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2830 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2831 that Gerrit footers are always at the end.
2832 """
2833 parsed_footer_line = git_footers.parse_footer(line)
2834 if parsed_footer_line:
2835 # Line is a gerrit footer in the form: Footer-Key: any value.
2836 # Thus, must be appended observing Gerrit footer rules.
2837 self.set_description(
2838 git_footers.add_footer(self.description,
2839 key=parsed_footer_line[0],
2840 value=parsed_footer_line[1]))
2841 return
2842
2843 if not self._description_lines:
2844 self._description_lines.append(line)
2845 return
2846
2847 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2848 if gerrit_footers:
2849 # git_footers.split_footers ensures that there is an empty line before
2850 # actual (gerrit) footers, if any. We have to keep it that way.
2851 assert top_lines and top_lines[-1] == ''
2852 top_lines, separator = top_lines[:-1], top_lines[-1:]
2853 else:
2854 separator = [] # No need for separator if there are no gerrit_footers.
2855
2856 prev_line = top_lines[-1] if top_lines else ''
2857 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2858 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2859 top_lines.append('')
2860 top_lines.append(line)
2861 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002862
tandrii99a72f22016-08-17 14:33:24 -07002863 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002864 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002865 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002866 reviewers = [match.group(2).strip()
2867 for match in matches
2868 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002869 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002870
2871
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002872def get_approving_reviewers(props):
2873 """Retrieves the reviewers that approved a CL from the issue properties with
2874 messages.
2875
2876 Note that the list may contain reviewers that are not committer, thus are not
2877 considered by the CQ.
2878 """
2879 return sorted(
2880 set(
2881 message['sender']
2882 for message in props['messages']
2883 if message['approval'] and message['sender'] in props['reviewers']
2884 )
2885 )
2886
2887
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002888def FindCodereviewSettingsFile(filename='codereview.settings'):
2889 """Finds the given file starting in the cwd and going up.
2890
2891 Only looks up to the top of the repository unless an
2892 'inherit-review-settings-ok' file exists in the root of the repository.
2893 """
2894 inherit_ok_file = 'inherit-review-settings-ok'
2895 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002896 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002897 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2898 root = '/'
2899 while True:
2900 if filename in os.listdir(cwd):
2901 if os.path.isfile(os.path.join(cwd, filename)):
2902 return open(os.path.join(cwd, filename))
2903 if cwd == root:
2904 break
2905 cwd = os.path.dirname(cwd)
2906
2907
2908def LoadCodereviewSettingsFromFile(fileobj):
2909 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002910 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002912 def SetProperty(name, setting, unset_error_ok=False):
2913 fullname = 'rietveld.' + name
2914 if setting in keyvals:
2915 RunGit(['config', fullname, keyvals[setting]])
2916 else:
2917 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2918
2919 SetProperty('server', 'CODE_REVIEW_SERVER')
2920 # Only server setting is required. Other settings can be absent.
2921 # In that case, we ignore errors raised during option deletion attempt.
2922 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002923 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002924 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2925 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002926 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002927 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002928 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2929 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002930 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002931 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002932 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002933 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2934 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002935
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002936 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002937 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002938
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002939 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002940 RunGit(['config', 'gerrit.squash-uploads',
2941 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002942
tandrii@chromium.org28253532016-04-14 13:46:56 +00002943 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002944 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002945 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2948 #should be of the form
2949 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2950 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2951 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2952 keyvals['ORIGIN_URL_CONFIG']])
2953
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002954
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002955def urlretrieve(source, destination):
2956 """urllib is broken for SSL connections via a proxy therefore we
2957 can't use urllib.urlretrieve()."""
2958 with open(destination, 'w') as f:
2959 f.write(urllib2.urlopen(source).read())
2960
2961
ukai@chromium.org712d6102013-11-27 00:52:58 +00002962def hasSheBang(fname):
2963 """Checks fname is a #! script."""
2964 with open(fname) as f:
2965 return f.read(2).startswith('#!')
2966
2967
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002968# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2969def DownloadHooks(*args, **kwargs):
2970 pass
2971
2972
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002973def DownloadGerritHook(force):
2974 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002975
2976 Args:
2977 force: True to update hooks. False to install hooks if not present.
2978 """
2979 if not settings.GetIsGerrit():
2980 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002981 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002982 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2983 if not os.access(dst, os.X_OK):
2984 if os.path.exists(dst):
2985 if not force:
2986 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002987 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002988 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002989 if not hasSheBang(dst):
2990 DieWithError('Not a script: %s\n'
2991 'You need to download from\n%s\n'
2992 'into .git/hooks/commit-msg and '
2993 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002994 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2995 except Exception:
2996 if os.path.exists(dst):
2997 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002998 DieWithError('\nFailed to download hooks.\n'
2999 'You need to download from\n%s\n'
3000 'into .git/hooks/commit-msg and '
3001 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003002
3003
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003004
3005def GetRietveldCodereviewSettingsInteractively():
3006 """Prompt the user for settings."""
3007 server = settings.GetDefaultServerUrl(error_ok=True)
3008 prompt = 'Rietveld server (host[:port])'
3009 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3010 newserver = ask_for_data(prompt + ':')
3011 if not server and not newserver:
3012 newserver = DEFAULT_SERVER
3013 if newserver:
3014 newserver = gclient_utils.UpgradeToHttps(newserver)
3015 if newserver != server:
3016 RunGit(['config', 'rietveld.server', newserver])
3017
3018 def SetProperty(initial, caption, name, is_url):
3019 prompt = caption
3020 if initial:
3021 prompt += ' ("x" to clear) [%s]' % initial
3022 new_val = ask_for_data(prompt + ':')
3023 if new_val == 'x':
3024 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3025 elif new_val:
3026 if is_url:
3027 new_val = gclient_utils.UpgradeToHttps(new_val)
3028 if new_val != initial:
3029 RunGit(['config', 'rietveld.' + name, new_val])
3030
3031 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3032 SetProperty(settings.GetDefaultPrivateFlag(),
3033 'Private flag (rietveld only)', 'private', False)
3034 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3035 'tree-status-url', False)
3036 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3037 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3038 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3039 'run-post-upload-hook', False)
3040
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003041@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003043 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003044
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003045 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003046 'For Gerrit, see http://crbug.com/603116.')
3047 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003048 parser.add_option('--activate-update', action='store_true',
3049 help='activate auto-updating [rietveld] section in '
3050 '.git/config')
3051 parser.add_option('--deactivate-update', action='store_true',
3052 help='deactivate auto-updating [rietveld] section in '
3053 '.git/config')
3054 options, args = parser.parse_args(args)
3055
3056 if options.deactivate_update:
3057 RunGit(['config', 'rietveld.autoupdate', 'false'])
3058 return
3059
3060 if options.activate_update:
3061 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3062 return
3063
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003064 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003065 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003066 return 0
3067
3068 url = args[0]
3069 if not url.endswith('codereview.settings'):
3070 url = os.path.join(url, 'codereview.settings')
3071
3072 # Load code review settings and download hooks (if available).
3073 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3074 return 0
3075
3076
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003077def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003078 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003079 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3080 branch = ShortBranchName(branchref)
3081 _, args = parser.parse_args(args)
3082 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003083 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003084 return RunGit(['config', 'branch.%s.base-url' % branch],
3085 error_ok=False).strip()
3086 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003087 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003088 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3089 error_ok=False).strip()
3090
3091
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003092def color_for_status(status):
3093 """Maps a Changelist status to color, for CMDstatus and other tools."""
3094 return {
3095 'unsent': Fore.RED,
3096 'waiting': Fore.BLUE,
3097 'reply': Fore.YELLOW,
3098 'lgtm': Fore.GREEN,
3099 'commit': Fore.MAGENTA,
3100 'closed': Fore.CYAN,
3101 'error': Fore.WHITE,
3102 }.get(status, Fore.WHITE)
3103
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003104
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003105def get_cl_statuses(changes, fine_grained, max_processes=None):
3106 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003107
3108 If fine_grained is true, this will fetch CL statuses from the server.
3109 Otherwise, simply indicate if there's a matching url for the given branches.
3110
3111 If max_processes is specified, it is used as the maximum number of processes
3112 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3113 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003114
3115 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003116 """
3117 # Silence upload.py otherwise it becomes unwieldly.
3118 upload.verbosity = 0
3119
3120 if fine_grained:
3121 # Process one branch synchronously to work through authentication, then
3122 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003123 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003124 def fetch(cl):
3125 try:
3126 return (cl, cl.GetStatus())
3127 except:
3128 # See http://crbug.com/629863.
3129 logging.exception('failed to fetch status for %s:', cl)
3130 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003131 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003132
tandriiea9514a2016-08-17 12:32:37 -07003133 changes_to_fetch = changes[1:]
3134 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003135 # Exit early if there was only one branch to fetch.
3136 return
3137
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003138 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003139 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003140 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003141 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003142
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003143 fetched_cls = set()
3144 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003145 while True:
3146 try:
3147 row = it.next(timeout=5)
3148 except multiprocessing.TimeoutError:
3149 break
3150
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003151 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003152 yield row
3153
3154 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003155 for cl in set(changes_to_fetch) - fetched_cls:
3156 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003157
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003158 else:
3159 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003160 for cl in changes:
3161 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003162
rmistry@google.com2dd99862015-06-22 12:22:18 +00003163
3164def upload_branch_deps(cl, args):
3165 """Uploads CLs of local branches that are dependents of the current branch.
3166
3167 If the local branch dependency tree looks like:
3168 test1 -> test2.1 -> test3.1
3169 -> test3.2
3170 -> test2.2 -> test3.3
3171
3172 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3173 run on the dependent branches in this order:
3174 test2.1, test3.1, test3.2, test2.2, test3.3
3175
3176 Note: This function does not rebase your local dependent branches. Use it when
3177 you make a change to the parent branch that will not conflict with its
3178 dependent branches, and you would like their dependencies updated in
3179 Rietveld.
3180 """
3181 if git_common.is_dirty_git_tree('upload-branch-deps'):
3182 return 1
3183
3184 root_branch = cl.GetBranch()
3185 if root_branch is None:
3186 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3187 'Get on a branch!')
3188 if not cl.GetIssue() or not cl.GetPatchset():
3189 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3190 'patchset dependencies without an uploaded CL.')
3191
3192 branches = RunGit(['for-each-ref',
3193 '--format=%(refname:short) %(upstream:short)',
3194 'refs/heads'])
3195 if not branches:
3196 print('No local branches found.')
3197 return 0
3198
3199 # Create a dictionary of all local branches to the branches that are dependent
3200 # on it.
3201 tracked_to_dependents = collections.defaultdict(list)
3202 for b in branches.splitlines():
3203 tokens = b.split()
3204 if len(tokens) == 2:
3205 branch_name, tracked = tokens
3206 tracked_to_dependents[tracked].append(branch_name)
3207
vapiera7fbd5a2016-06-16 09:17:49 -07003208 print()
3209 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003210 dependents = []
3211 def traverse_dependents_preorder(branch, padding=''):
3212 dependents_to_process = tracked_to_dependents.get(branch, [])
3213 padding += ' '
3214 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003215 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003216 dependents.append(dependent)
3217 traverse_dependents_preorder(dependent, padding)
3218 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003219 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003220
3221 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003222 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003223 return 0
3224
vapiera7fbd5a2016-06-16 09:17:49 -07003225 print('This command will checkout all dependent branches and run '
3226 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003227 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3228
andybons@chromium.org962f9462016-02-03 20:00:42 +00003229 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003230 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003231 args.extend(['-t', 'Updated patchset dependency'])
3232
rmistry@google.com2dd99862015-06-22 12:22:18 +00003233 # Record all dependents that failed to upload.
3234 failures = {}
3235 # Go through all dependents, checkout the branch and upload.
3236 try:
3237 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003238 print()
3239 print('--------------------------------------')
3240 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003241 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003242 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003243 try:
3244 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003245 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003246 failures[dependent_branch] = 1
3247 except: # pylint: disable=W0702
3248 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003249 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003250 finally:
3251 # Swap back to the original root branch.
3252 RunGit(['checkout', '-q', root_branch])
3253
vapiera7fbd5a2016-06-16 09:17:49 -07003254 print()
3255 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003256 for dependent_branch in dependents:
3257 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003258 print(' %s : %s' % (dependent_branch, upload_status))
3259 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003260
3261 return 0
3262
3263
kmarshall3bff56b2016-06-06 18:31:47 -07003264def CMDarchive(parser, args):
3265 """Archives and deletes branches associated with closed changelists."""
3266 parser.add_option(
3267 '-j', '--maxjobs', action='store', type=int,
3268 help='The maximum number of jobs to use when retrieving review status')
3269 parser.add_option(
3270 '-f', '--force', action='store_true',
3271 help='Bypasses the confirmation prompt.')
3272
3273 auth.add_auth_options(parser)
3274 options, args = parser.parse_args(args)
3275 if args:
3276 parser.error('Unsupported args: %s' % ' '.join(args))
3277 auth_config = auth.extract_auth_config_from_options(options)
3278
3279 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3280 if not branches:
3281 return 0
3282
vapiera7fbd5a2016-06-16 09:17:49 -07003283 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003284 changes = [Changelist(branchref=b, auth_config=auth_config)
3285 for b in branches.splitlines()]
3286 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3287 statuses = get_cl_statuses(changes,
3288 fine_grained=True,
3289 max_processes=options.maxjobs)
3290 proposal = [(cl.GetBranch(),
3291 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3292 for cl, status in statuses
3293 if status == 'closed']
3294 proposal.sort()
3295
3296 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003297 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003298 return 0
3299
3300 current_branch = GetCurrentBranch()
3301
vapiera7fbd5a2016-06-16 09:17:49 -07003302 print('\nBranches with closed issues that will be archived:\n')
3303 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003304 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003305 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003306
3307 if any(branch == current_branch for branch, _ in proposal):
3308 print('You are currently on a branch \'%s\' which is associated with a '
3309 'closed codereview issue, so archive cannot proceed. Please '
3310 'checkout another branch and run this command again.' %
3311 current_branch)
3312 return 1
3313
3314 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003315 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3316 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003317 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003318 return 1
3319
3320 for branch, tagname in proposal:
3321 RunGit(['tag', tagname, branch])
3322 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003323 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003324
3325 return 0
3326
3327
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003328def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003329 """Show status of changelists.
3330
3331 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003332 - Red not sent for review or broken
3333 - Blue waiting for review
3334 - Yellow waiting for you to reply to review
3335 - Green LGTM'ed
3336 - Magenta in the commit queue
3337 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003338
3339 Also see 'git cl comments'.
3340 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003341 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003342 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003343 parser.add_option('-f', '--fast', action='store_true',
3344 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003345 parser.add_option(
3346 '-j', '--maxjobs', action='store', type=int,
3347 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003348
3349 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003350 _add_codereview_issue_select_options(
3351 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003352 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003353 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003354 if args:
3355 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003356 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003357
iannuccie53c9352016-08-17 14:40:40 -07003358 if options.issue is not None and not options.field:
3359 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003360
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003361 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003362 cl = Changelist(auth_config=auth_config, issue=options.issue,
3363 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003364 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003365 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003366 elif options.field == 'id':
3367 issueid = cl.GetIssue()
3368 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003369 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003370 elif options.field == 'patch':
3371 patchset = cl.GetPatchset()
3372 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003373 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003374 elif options.field == 'status':
3375 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003376 elif options.field == 'url':
3377 url = cl.GetIssueURL()
3378 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003379 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003380 return 0
3381
3382 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3383 if not branches:
3384 print('No local branch found.')
3385 return 0
3386
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003387 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003388 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003389 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003390 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003391 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003392 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003393 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003394
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003395 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003396 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3397 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3398 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003399 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003400 c, status = output.next()
3401 branch_statuses[c.GetBranch()] = status
3402 status = branch_statuses.pop(branch)
3403 url = cl.GetIssueURL()
3404 if url and (not status or status == 'error'):
3405 # The issue probably doesn't exist anymore.
3406 url += ' (broken)'
3407
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003408 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003409 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003410 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003411 color = ''
3412 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003413 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003414 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003415 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003416 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003417
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003418 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003419 print()
3420 print('Current branch:',)
3421 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003422 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003423 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003424 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003425 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003426 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003427 print('Issue description:')
3428 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003429 return 0
3430
3431
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003432def colorize_CMDstatus_doc():
3433 """To be called once in main() to add colors to git cl status help."""
3434 colors = [i for i in dir(Fore) if i[0].isupper()]
3435
3436 def colorize_line(line):
3437 for color in colors:
3438 if color in line.upper():
3439 # Extract whitespaces first and the leading '-'.
3440 indent = len(line) - len(line.lstrip(' ')) + 1
3441 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3442 return line
3443
3444 lines = CMDstatus.__doc__.splitlines()
3445 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3446
3447
phajdan.jre328cf92016-08-22 04:12:17 -07003448def write_json(path, contents):
3449 with open(path, 'w') as f:
3450 json.dump(contents, f)
3451
3452
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003453@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003454def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003455 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003456
3457 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003458 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003459 parser.add_option('-r', '--reverse', action='store_true',
3460 help='Lookup the branch(es) for the specified issues. If '
3461 'no issues are specified, all branches with mapped '
3462 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003463 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003464 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003465 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003466 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003467
dnj@chromium.org406c4402015-03-03 17:22:28 +00003468 if options.reverse:
3469 branches = RunGit(['for-each-ref', 'refs/heads',
3470 '--format=%(refname:short)']).splitlines()
3471
3472 # Reverse issue lookup.
3473 issue_branch_map = {}
3474 for branch in branches:
3475 cl = Changelist(branchref=branch)
3476 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3477 if not args:
3478 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003479 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003480 for issue in args:
3481 if not issue:
3482 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003483 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003484 print('Branch for issue number %s: %s' % (
3485 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003486 if options.json:
3487 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003488 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003489 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003490 if len(args) > 0:
3491 try:
3492 issue = int(args[0])
3493 except ValueError:
3494 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003495 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003496 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003497 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003498 if options.json:
3499 write_json(options.json, {
3500 'issue': cl.GetIssue(),
3501 'issue_url': cl.GetIssueURL(),
3502 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003503 return 0
3504
3505
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003506def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003507 """Shows or posts review comments for any changelist."""
3508 parser.add_option('-a', '--add-comment', dest='comment',
3509 help='comment to add to an issue')
3510 parser.add_option('-i', dest='issue',
3511 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003512 parser.add_option('-j', '--json-file',
3513 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003514 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003515 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003516 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003517
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003518 issue = None
3519 if options.issue:
3520 try:
3521 issue = int(options.issue)
3522 except ValueError:
3523 DieWithError('A review issue id is expected to be a number')
3524
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003525 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003526
3527 if options.comment:
3528 cl.AddComment(options.comment)
3529 return 0
3530
3531 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003532 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003533 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003534 summary.append({
3535 'date': message['date'],
3536 'lgtm': False,
3537 'message': message['text'],
3538 'not_lgtm': False,
3539 'sender': message['sender'],
3540 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003541 if message['disapproval']:
3542 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003543 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003544 elif message['approval']:
3545 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003546 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003547 elif message['sender'] == data['owner_email']:
3548 color = Fore.MAGENTA
3549 else:
3550 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003551 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003552 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003553 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003554 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003555 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003556 if options.json_file:
3557 with open(options.json_file, 'wb') as f:
3558 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003559 return 0
3560
3561
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003562@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003563def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003564 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003565 parser.add_option('-d', '--display', action='store_true',
3566 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003567 parser.add_option('-n', '--new-description',
3568 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003569
3570 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003571 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003572 options, args = parser.parse_args(args)
3573 _process_codereview_select_options(parser, options)
3574
3575 target_issue = None
3576 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003577 target_issue = ParseIssueNumberArgument(args[0])
3578 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003579 parser.print_help()
3580 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003581
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003582 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003583
martiniss6eda05f2016-06-30 10:18:35 -07003584 kwargs = {
3585 'auth_config': auth_config,
3586 'codereview': options.forced_codereview,
3587 }
3588 if target_issue:
3589 kwargs['issue'] = target_issue.issue
3590 if options.forced_codereview == 'rietveld':
3591 kwargs['rietveld_server'] = target_issue.hostname
3592
3593 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003594
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003595 if not cl.GetIssue():
3596 DieWithError('This branch has no associated changelist.')
3597 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003598
smut@google.com34fb6b12015-07-13 20:03:26 +00003599 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003600 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003601 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003602
3603 if options.new_description:
3604 text = options.new_description
3605 if text == '-':
3606 text = '\n'.join(l.rstrip() for l in sys.stdin)
3607
3608 description.set_description(text)
3609 else:
3610 description.prompt()
3611
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003612 if cl.GetDescription() != description.description:
3613 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003614 return 0
3615
3616
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003617def CreateDescriptionFromLog(args):
3618 """Pulls out the commit log to use as a base for the CL description."""
3619 log_args = []
3620 if len(args) == 1 and not args[0].endswith('.'):
3621 log_args = [args[0] + '..']
3622 elif len(args) == 1 and args[0].endswith('...'):
3623 log_args = [args[0][:-1]]
3624 elif len(args) == 2:
3625 log_args = [args[0] + '..' + args[1]]
3626 else:
3627 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003628 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629
3630
thestig@chromium.org44202a22014-03-11 19:22:18 +00003631def CMDlint(parser, args):
3632 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003633 parser.add_option('--filter', action='append', metavar='-x,+y',
3634 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003635 auth.add_auth_options(parser)
3636 options, args = parser.parse_args(args)
3637 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003638
3639 # Access to a protected member _XX of a client class
3640 # pylint: disable=W0212
3641 try:
3642 import cpplint
3643 import cpplint_chromium
3644 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003645 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003646 return 1
3647
3648 # Change the current working directory before calling lint so that it
3649 # shows the correct base.
3650 previous_cwd = os.getcwd()
3651 os.chdir(settings.GetRoot())
3652 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003653 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003654 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3655 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003656 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003657 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003658 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003659
3660 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003661 command = args + files
3662 if options.filter:
3663 command = ['--filter=' + ','.join(options.filter)] + command
3664 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003665
3666 white_regex = re.compile(settings.GetLintRegex())
3667 black_regex = re.compile(settings.GetLintIgnoreRegex())
3668 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3669 for filename in filenames:
3670 if white_regex.match(filename):
3671 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003672 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003673 else:
3674 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3675 extra_check_functions)
3676 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003677 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003678 finally:
3679 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003680 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003681 if cpplint._cpplint_state.error_count != 0:
3682 return 1
3683 return 0
3684
3685
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003686def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003687 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003688 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003689 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003690 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003691 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003692 auth.add_auth_options(parser)
3693 options, args = parser.parse_args(args)
3694 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003695
sbc@chromium.org71437c02015-04-09 19:29:40 +00003696 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003697 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003698 return 1
3699
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003700 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003701 if args:
3702 base_branch = args[0]
3703 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003704 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003705 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003706
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003707 cl.RunHook(
3708 committing=not options.upload,
3709 may_prompt=False,
3710 verbose=options.verbose,
3711 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003712 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003713
3714
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003715def GenerateGerritChangeId(message):
3716 """Returns Ixxxxxx...xxx change id.
3717
3718 Works the same way as
3719 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3720 but can be called on demand on all platforms.
3721
3722 The basic idea is to generate git hash of a state of the tree, original commit
3723 message, author/committer info and timestamps.
3724 """
3725 lines = []
3726 tree_hash = RunGitSilent(['write-tree'])
3727 lines.append('tree %s' % tree_hash.strip())
3728 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3729 if code == 0:
3730 lines.append('parent %s' % parent.strip())
3731 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3732 lines.append('author %s' % author.strip())
3733 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3734 lines.append('committer %s' % committer.strip())
3735 lines.append('')
3736 # Note: Gerrit's commit-hook actually cleans message of some lines and
3737 # whitespace. This code is not doing this, but it clearly won't decrease
3738 # entropy.
3739 lines.append(message)
3740 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3741 stdin='\n'.join(lines))
3742 return 'I%s' % change_hash.strip()
3743
3744
wittman@chromium.org455dc922015-01-26 20:15:50 +00003745def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3746 """Computes the remote branch ref to use for the CL.
3747
3748 Args:
3749 remote (str): The git remote for the CL.
3750 remote_branch (str): The git remote branch for the CL.
3751 target_branch (str): The target branch specified by the user.
3752 pending_prefix (str): The pending prefix from the settings.
3753 """
3754 if not (remote and remote_branch):
3755 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003756
wittman@chromium.org455dc922015-01-26 20:15:50 +00003757 if target_branch:
3758 # Cannonicalize branch references to the equivalent local full symbolic
3759 # refs, which are then translated into the remote full symbolic refs
3760 # below.
3761 if '/' not in target_branch:
3762 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3763 else:
3764 prefix_replacements = (
3765 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3766 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3767 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3768 )
3769 match = None
3770 for regex, replacement in prefix_replacements:
3771 match = re.search(regex, target_branch)
3772 if match:
3773 remote_branch = target_branch.replace(match.group(0), replacement)
3774 break
3775 if not match:
3776 # This is a branch path but not one we recognize; use as-is.
3777 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003778 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3779 # Handle the refs that need to land in different refs.
3780 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003781
wittman@chromium.org455dc922015-01-26 20:15:50 +00003782 # Create the true path to the remote branch.
3783 # Does the following translation:
3784 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3785 # * refs/remotes/origin/master -> refs/heads/master
3786 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3787 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3788 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3789 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3790 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3791 'refs/heads/')
3792 elif remote_branch.startswith('refs/remotes/branch-heads'):
3793 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3794 # If a pending prefix exists then replace refs/ with it.
3795 if pending_prefix:
3796 remote_branch = remote_branch.replace('refs/', pending_prefix)
3797 return remote_branch
3798
3799
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003800def cleanup_list(l):
3801 """Fixes a list so that comma separated items are put as individual items.
3802
3803 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3804 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3805 """
3806 items = sum((i.split(',') for i in l), [])
3807 stripped_items = (i.strip() for i in items)
3808 return sorted(filter(None, stripped_items))
3809
3810
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003811@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003812def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003813 """Uploads the current changelist to codereview.
3814
3815 Can skip dependency patchset uploads for a branch by running:
3816 git config branch.branch_name.skip-deps-uploads True
3817 To unset run:
3818 git config --unset branch.branch_name.skip-deps-uploads
3819 Can also set the above globally by using the --global flag.
3820 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003821 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3822 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003823 parser.add_option('--bypass-watchlists', action='store_true',
3824 dest='bypass_watchlists',
3825 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003826 parser.add_option('-f', action='store_true', dest='force',
3827 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003828 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003829 parser.add_option('-b', '--bug',
3830 help='pre-populate the bug number(s) for this issue. '
3831 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003832 parser.add_option('--message-file', dest='message_file',
3833 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003834 parser.add_option('-t', dest='title',
3835 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003836 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003837 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003838 help='reviewer email addresses')
3839 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003840 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003841 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003842 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003843 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003844 parser.add_option('--emulate_svn_auto_props',
3845 '--emulate-svn-auto-props',
3846 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003847 dest="emulate_svn_auto_props",
3848 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003849 parser.add_option('-c', '--use-commit-queue', action='store_true',
3850 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003851 parser.add_option('--private', action='store_true',
3852 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003853 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003854 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003855 metavar='TARGET',
3856 help='Apply CL to remote ref TARGET. ' +
3857 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003858 parser.add_option('--squash', action='store_true',
3859 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003860 parser.add_option('--no-squash', action='store_true',
3861 help='Don\'t squash multiple commits into one ' +
3862 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003863 parser.add_option('--email', default=None,
3864 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003865 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3866 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003867 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3868 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003869 help='Send the patchset to do a CQ dry run right after '
3870 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003871 parser.add_option('--dependencies', action='store_true',
3872 help='Uploads CLs of all the local branches that depend on '
3873 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003874
rmistry@google.com2dd99862015-06-22 12:22:18 +00003875 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003876 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003877 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003878 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003879 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003880 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003881 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003882
sbc@chromium.org71437c02015-04-09 19:29:40 +00003883 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003884 return 1
3885
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003886 options.reviewers = cleanup_list(options.reviewers)
3887 options.cc = cleanup_list(options.cc)
3888
tandriib80458a2016-06-23 12:20:07 -07003889 if options.message_file:
3890 if options.message:
3891 parser.error('only one of --message and --message-file allowed.')
3892 options.message = gclient_utils.FileRead(options.message_file)
3893 options.message_file = None
3894
tandrii4d0545a2016-07-06 03:56:49 -07003895 if options.cq_dry_run and options.use_commit_queue:
3896 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3897
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003898 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3899 settings.GetIsGerrit()
3900
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003901 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003902 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003903
3904
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003905def IsSubmoduleMergeCommit(ref):
3906 # When submodules are added to the repo, we expect there to be a single
3907 # non-git-svn merge commit at remote HEAD with a signature comment.
3908 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003909 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003910 return RunGit(cmd) != ''
3911
3912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003913def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003914 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003915
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003916 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3917 upstream and closes the issue automatically and atomically.
3918
3919 Otherwise (in case of Rietveld):
3920 Squashes branch into a single commit.
3921 Updates changelog with metadata (e.g. pointer to review).
3922 Pushes/dcommits the code upstream.
3923 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924 """
3925 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3926 help='bypass upload presubmit hook')
3927 parser.add_option('-m', dest='message',
3928 help="override review description")
3929 parser.add_option('-f', action='store_true', dest='force',
3930 help="force yes to questions (don't prompt)")
3931 parser.add_option('-c', dest='contributor',
3932 help="external contributor for patch (appended to " +
3933 "description and used as author for git). Should be " +
3934 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003935 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003936 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003937 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003938 auth_config = auth.extract_auth_config_from_options(options)
3939
3940 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003941
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003942 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3943 if cl.IsGerrit():
3944 if options.message:
3945 # This could be implemented, but it requires sending a new patch to
3946 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3947 # Besides, Gerrit has the ability to change the commit message on submit
3948 # automatically, thus there is no need to support this option (so far?).
3949 parser.error('-m MESSAGE option is not supported for Gerrit.')
3950 if options.contributor:
3951 parser.error(
3952 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3953 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3954 'the contributor\'s "name <email>". If you can\'t upload such a '
3955 'commit for review, contact your repository admin and request'
3956 '"Forge-Author" permission.')
3957 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3958 options.verbose)
3959
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003960 current = cl.GetBranch()
3961 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3962 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003963 print()
3964 print('Attempting to push branch %r into another local branch!' % current)
3965 print()
3966 print('Either reparent this branch on top of origin/master:')
3967 print(' git reparent-branch --root')
3968 print()
3969 print('OR run `git rebase-update` if you think the parent branch is ')
3970 print('already committed.')
3971 print()
3972 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003973 return 1
3974
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003975 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003976 # Default to merging against our best guess of the upstream branch.
3977 args = [cl.GetUpstreamBranch()]
3978
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003979 if options.contributor:
3980 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003981 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003982 return 1
3983
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003985 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986
sbc@chromium.org71437c02015-04-09 19:29:40 +00003987 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003988 return 1
3989
3990 # This rev-list syntax means "show all commits not in my branch that
3991 # are in base_branch".
3992 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3993 base_branch]).splitlines()
3994 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003995 print('Base branch "%s" has %d commits '
3996 'not in this branch.' % (base_branch, len(upstream_commits)))
3997 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998 return 1
3999
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004000 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004001 svn_head = None
4002 if cmd == 'dcommit' or base_has_submodules:
4003 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4004 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004007 # If the base_head is a submodule merge commit, the first parent of the
4008 # base_head should be a git-svn commit, which is what we're interested in.
4009 base_svn_head = base_branch
4010 if base_has_submodules:
4011 base_svn_head += '^1'
4012
4013 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004014 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004015 print('This branch has %d additional commits not upstreamed yet.'
4016 % len(extra_commits.splitlines()))
4017 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4018 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004019 return 1
4020
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004021 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004022 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004023 author = None
4024 if options.contributor:
4025 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004026 hook_results = cl.RunHook(
4027 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004028 may_prompt=not options.force,
4029 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004030 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004031 if not hook_results.should_continue():
4032 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004033
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004034 # Check the tree status if the tree status URL is set.
4035 status = GetTreeStatus()
4036 if 'closed' == status:
4037 print('The tree is closed. Please wait for it to reopen. Use '
4038 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4039 return 1
4040 elif 'unknown' == status:
4041 print('Unable to determine tree status. Please verify manually and '
4042 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4043 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004045 change_desc = ChangeDescription(options.message)
4046 if not change_desc.description and cl.GetIssue():
4047 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004049 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004050 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004051 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004052 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004053 print('No description set.')
4054 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004055 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004056
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004057 # Keep a separate copy for the commit message, because the commit message
4058 # contains the link to the Rietveld issue, while the Rietveld message contains
4059 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004060 # Keep a separate copy for the commit message.
4061 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004062 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004063
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004064 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004065 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004066 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004067 # after it. Add a period on a new line to circumvent this. Also add a space
4068 # before the period to make sure that Gitiles continues to correctly resolve
4069 # the URL.
4070 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004071 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004072 commit_desc.append_footer('Patch from %s.' % options.contributor)
4073
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004074 print('Description:')
4075 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004077 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004078 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004079 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004081 # We want to squash all this branch's commits into one commit with the proper
4082 # description. We do this by doing a "reset --soft" to the base branch (which
4083 # keeps the working copy the same), then dcommitting that. If origin/master
4084 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4085 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004087 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4088 # Delete the branches if they exist.
4089 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4090 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4091 result = RunGitWithCode(showref_cmd)
4092 if result[0] == 0:
4093 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004094
4095 # We might be in a directory that's present in this branch but not in the
4096 # trunk. Move up to the top of the tree so that git commands that expect a
4097 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004098 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004099 if rel_base_path:
4100 os.chdir(rel_base_path)
4101
4102 # Stuff our change into the merge branch.
4103 # We wrap in a try...finally block so if anything goes wrong,
4104 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004105 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004106 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004107 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004108 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004109 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004110 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004111 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004112 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004113 RunGit(
4114 [
4115 'commit', '--author', options.contributor,
4116 '-m', commit_desc.description,
4117 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004119 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004120 if base_has_submodules:
4121 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4122 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4123 RunGit(['checkout', CHERRY_PICK_BRANCH])
4124 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004125 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004126 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004127 mirror = settings.GetGitMirror(remote)
4128 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004129 pending_prefix = settings.GetPendingRefPrefix()
4130 if not pending_prefix or branch.startswith(pending_prefix):
4131 # If not using refs/pending/heads/* at all, or target ref is already set
4132 # to pending, then push to the target ref directly.
4133 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004134 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004135 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004136 else:
4137 # Cherry-pick the change on top of pending ref and then push it.
4138 assert branch.startswith('refs/'), branch
4139 assert pending_prefix[-1] == '/', pending_prefix
4140 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004141 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004142 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004143 if retcode == 0:
4144 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004145 else:
4146 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004147 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004148 'svn', 'dcommit',
4149 '-C%s' % options.similarity,
4150 '--no-rebase', '--rmdir',
4151 ]
4152 if settings.GetForceHttpsCommitUrl():
4153 # Allow forcing https commit URLs for some projects that don't allow
4154 # committing to http URLs (like Google Code).
4155 remote_url = cl.GetGitSvnRemoteUrl()
4156 if urlparse.urlparse(remote_url).scheme == 'http':
4157 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004158 cmd_args.append('--commit-url=%s' % remote_url)
4159 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004160 if 'Committed r' in output:
4161 revision = re.match(
4162 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4163 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004164 finally:
4165 # And then swap back to the original branch and clean up.
4166 RunGit(['checkout', '-q', cl.GetBranch()])
4167 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004168 if base_has_submodules:
4169 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004170
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004171 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004172 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004173 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004174
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004175 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004176 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004177 try:
4178 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4179 # We set pushed_to_pending to False, since it made it all the way to the
4180 # real ref.
4181 pushed_to_pending = False
4182 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004183 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004186 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004187 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004188 if not to_pending:
4189 if viewvc_url and revision:
4190 change_desc.append_footer(
4191 'Committed: %s%s' % (viewvc_url, revision))
4192 elif revision:
4193 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004194 print('Closing issue '
4195 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004196 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004197 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004198 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004199 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004200 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004201 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004202 if options.bypass_hooks:
4203 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4204 else:
4205 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004206 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004207
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004208 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004209 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004210 print('The commit is in the pending queue (%s).' % pending_ref)
4211 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4212 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004213
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004214 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4215 if os.path.isfile(hook):
4216 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004217
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004218 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004219
4220
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004221def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004222 print()
4223 print('Waiting for commit to be landed on %s...' % real_ref)
4224 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004225 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4226 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004227 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004228
4229 loop = 0
4230 while True:
4231 sys.stdout.write('fetching (%d)... \r' % loop)
4232 sys.stdout.flush()
4233 loop += 1
4234
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004235 if mirror:
4236 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004237 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4238 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4239 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4240 for commit in commits.splitlines():
4241 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004242 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004243 return commit
4244
4245 current_rev = to_rev
4246
4247
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004248def PushToGitPending(remote, pending_ref, upstream_ref):
4249 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4250
4251 Returns:
4252 (retcode of last operation, output log of last operation).
4253 """
4254 assert pending_ref.startswith('refs/'), pending_ref
4255 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4256 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4257 code = 0
4258 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004259 max_attempts = 3
4260 attempts_left = max_attempts
4261 while attempts_left:
4262 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004263 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004264 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004265
4266 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004267 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004268 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004269 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004270 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004271 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004272 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004273 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004274 continue
4275
4276 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004277 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004278 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004279 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004280 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004281 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4282 'the following files have merge conflicts:' % pending_ref)
4283 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4284 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004285 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004286 return code, out
4287
4288 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004289 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004290 code, out = RunGitWithCode(
4291 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4292 if code == 0:
4293 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004294 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004295 return code, out
4296
vapiera7fbd5a2016-06-16 09:17:49 -07004297 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004298 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004299 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004300 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004301 print('Fatal push error. Make sure your .netrc credentials and git '
4302 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004303 return code, out
4304
vapiera7fbd5a2016-06-16 09:17:49 -07004305 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004306 return code, out
4307
4308
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004309def IsFatalPushFailure(push_stdout):
4310 """True if retrying push won't help."""
4311 return '(prohibited by Gerrit)' in push_stdout
4312
4313
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004314@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004316 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004317 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004318 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004319 # If it looks like previous commits were mirrored with git-svn.
4320 message = """This repository appears to be a git-svn mirror, but no
4321upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4322 else:
4323 message = """This doesn't appear to be an SVN repository.
4324If your project has a true, writeable git repository, you probably want to run
4325'git cl land' instead.
4326If your project has a git mirror of an upstream SVN master, you probably need
4327to run 'git svn init'.
4328
4329Using the wrong command might cause your commit to appear to succeed, and the
4330review to be closed, without actually landing upstream. If you choose to
4331proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004332 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004333 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004334 # TODO(tandrii): kill this post SVN migration with
4335 # https://codereview.chromium.org/2076683002
4336 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4337 'Please let us know of this project you are committing to:'
4338 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004339 return SendUpstream(parser, args, 'dcommit')
4340
4341
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004342@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004343def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004344 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004345 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004346 print('This appears to be an SVN repository.')
4347 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004348 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004349 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004350 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004351
4352
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004353@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004354def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004355 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004356 parser.add_option('-b', dest='newbranch',
4357 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004358 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004359 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004360 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4361 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004362 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004363 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004364 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004365 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004366 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004367 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004368
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004369
4370 group = optparse.OptionGroup(
4371 parser,
4372 'Options for continuing work on the current issue uploaded from a '
4373 'different clone (e.g. different machine). Must be used independently '
4374 'from the other options. No issue number should be specified, and the '
4375 'branch must have an issue number associated with it')
4376 group.add_option('--reapply', action='store_true', dest='reapply',
4377 help='Reset the branch and reapply the issue.\n'
4378 'CAUTION: This will undo any local changes in this '
4379 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004380
4381 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004382 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004383 parser.add_option_group(group)
4384
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004385 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004386 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004388 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004389 auth_config = auth.extract_auth_config_from_options(options)
4390
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004391
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004392 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004393 if options.newbranch:
4394 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004395 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004396 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004397
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004398 cl = Changelist(auth_config=auth_config,
4399 codereview=options.forced_codereview)
4400 if not cl.GetIssue():
4401 parser.error('current branch must have an associated issue')
4402
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004403 upstream = cl.GetUpstreamBranch()
4404 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004405 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004406
4407 RunGit(['reset', '--hard', upstream])
4408 if options.pull:
4409 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004410
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004411 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4412 options.directory)
4413
4414 if len(args) != 1 or not args[0]:
4415 parser.error('Must specify issue number or url')
4416
4417 # We don't want uncommitted changes mixed up with the patch.
4418 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004419 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004420
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004421 if options.newbranch:
4422 if options.force:
4423 RunGit(['branch', '-D', options.newbranch],
4424 stderr=subprocess2.PIPE, error_ok=True)
4425 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004426 elif not GetCurrentBranch():
4427 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004428
4429 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4430
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004431 if cl.IsGerrit():
4432 if options.reject:
4433 parser.error('--reject is not supported with Gerrit codereview.')
4434 if options.nocommit:
4435 parser.error('--nocommit is not supported with Gerrit codereview.')
4436 if options.directory:
4437 parser.error('--directory is not supported with Gerrit codereview.')
4438
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004439 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004440 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004441
4442
4443def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004444 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004445 # Provide a wrapper for git svn rebase to help avoid accidental
4446 # git svn dcommit.
4447 # It's the only command that doesn't use parser at all since we just defer
4448 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004449
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004450 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004451
4452
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004453def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004454 """Fetches the tree status and returns either 'open', 'closed',
4455 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004456 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004457 if url:
4458 status = urllib2.urlopen(url).read().lower()
4459 if status.find('closed') != -1 or status == '0':
4460 return 'closed'
4461 elif status.find('open') != -1 or status == '1':
4462 return 'open'
4463 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004464 return 'unset'
4465
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004466
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004467def GetTreeStatusReason():
4468 """Fetches the tree status from a json url and returns the message
4469 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004470 url = settings.GetTreeStatusUrl()
4471 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472 connection = urllib2.urlopen(json_url)
4473 status = json.loads(connection.read())
4474 connection.close()
4475 return status['message']
4476
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004477
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004478def GetBuilderMaster(bot_list):
4479 """For a given builder, fetch the master from AE if available."""
4480 map_url = 'https://builders-map.appspot.com/'
4481 try:
4482 master_map = json.load(urllib2.urlopen(map_url))
4483 except urllib2.URLError as e:
4484 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4485 (map_url, e))
4486 except ValueError as e:
4487 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4488 if not master_map:
4489 return None, 'Failed to build master map.'
4490
4491 result_master = ''
4492 for bot in bot_list:
4493 builder = bot.split(':', 1)[0]
4494 master_list = master_map.get(builder, [])
4495 if not master_list:
4496 return None, ('No matching master for builder %s.' % builder)
4497 elif len(master_list) > 1:
4498 return None, ('The builder name %s exists in multiple masters %s.' %
4499 (builder, master_list))
4500 else:
4501 cur_master = master_list[0]
4502 if not result_master:
4503 result_master = cur_master
4504 elif result_master != cur_master:
4505 return None, 'The builders do not belong to the same master.'
4506 return result_master, None
4507
4508
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004509def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004510 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004511 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004512 status = GetTreeStatus()
4513 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004514 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004515 return 2
4516
vapiera7fbd5a2016-06-16 09:17:49 -07004517 print('The tree is %s' % status)
4518 print()
4519 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004520 if status != 'open':
4521 return 1
4522 return 0
4523
4524
maruel@chromium.org15192402012-09-06 12:38:29 +00004525def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004526 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004527 group = optparse.OptionGroup(parser, "Try job options")
4528 group.add_option(
4529 "-b", "--bot", action="append",
4530 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4531 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004532 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004533 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004534 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004535 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004536 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004537 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004538 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004539 "-r", "--revision",
4540 help="Revision to use for the try job; default: the "
4541 "revision will be determined by the try server; see "
4542 "its waterfall for more info")
4543 group.add_option(
4544 "-c", "--clobber", action="store_true", default=False,
4545 help="Force a clobber before building; e.g. don't do an "
4546 "incremental build")
4547 group.add_option(
4548 "--project",
4549 help="Override which project to use. Projects are defined "
4550 "server-side to define what default bot set to use")
4551 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004552 "-p", "--property", dest="properties", action="append", default=[],
4553 help="Specify generic properties in the form -p key1=value1 -p "
4554 "key2=value2 etc (buildbucket only). The value will be treated as "
4555 "json if decodable, or as string otherwise.")
4556 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004557 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004558 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004559 "--use-rietveld", action="store_true", default=False,
4560 help="Use Rietveld to trigger try jobs.")
4561 group.add_option(
4562 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4563 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004564 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004565 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004566 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004567 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004568
machenbach@chromium.org45453142015-09-15 08:45:22 +00004569 if options.use_rietveld and options.properties:
4570 parser.error('Properties can only be specified with buildbucket')
4571
4572 # Make sure that all properties are prop=value pairs.
4573 bad_params = [x for x in options.properties if '=' not in x]
4574 if bad_params:
4575 parser.error('Got properties with missing "=": %s' % bad_params)
4576
maruel@chromium.org15192402012-09-06 12:38:29 +00004577 if args:
4578 parser.error('Unknown arguments: %s' % args)
4579
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004580 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004581 if not cl.GetIssue():
4582 parser.error('Need to upload first')
4583
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004584 if cl.IsGerrit():
4585 parser.error(
4586 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4587 'If your project has Commit Queue, dry run is a workaround:\n'
4588 ' git cl set-commit --dry-run')
4589 # Code below assumes Rietveld issue.
4590 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4591
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004592 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004593 if props.get('closed'):
4594 parser.error('Cannot send tryjobs for a closed CL')
4595
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004596 if props.get('private'):
4597 parser.error('Cannot use trybots with private issue')
4598
maruel@chromium.org15192402012-09-06 12:38:29 +00004599 if not options.name:
4600 options.name = cl.GetBranch()
4601
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004602 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004603 options.master, err_msg = GetBuilderMaster(options.bot)
4604 if err_msg:
4605 parser.error('Tryserver master cannot be found because: %s\n'
4606 'Please manually specify the tryserver master'
4607 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004608
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004609 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004610 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004611 if not options.bot:
4612 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004613
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004614 # Get try masters from PRESUBMIT.py files.
4615 masters = presubmit_support.DoGetTryMasters(
4616 change,
4617 change.LocalPaths(),
4618 settings.GetRoot(),
4619 None,
4620 None,
4621 options.verbose,
4622 sys.stdout)
4623 if masters:
4624 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004625
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004626 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4627 options.bot = presubmit_support.DoGetTrySlaves(
4628 change,
4629 change.LocalPaths(),
4630 settings.GetRoot(),
4631 None,
4632 None,
4633 options.verbose,
4634 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004635
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004636 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004637 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004638
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004639 builders_and_tests = {}
4640 # TODO(machenbach): The old style command-line options don't support
4641 # multiple try masters yet.
4642 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4643 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4644
4645 for bot in old_style:
4646 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004647 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004648 elif ',' in bot:
4649 parser.error('Specify one bot per --bot flag')
4650 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004651 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004652
4653 for bot, tests in new_style:
4654 builders_and_tests.setdefault(bot, []).extend(tests)
4655
4656 # Return a master map with one master to be backwards compatible. The
4657 # master name defaults to an empty string, which will cause the master
4658 # not to be set on rietveld (deprecated).
4659 return {options.master: builders_and_tests}
4660
4661 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004662 if not masters:
4663 # Default to triggering Dry Run (see http://crbug.com/625697).
4664 if options.verbose:
4665 print('git cl try with no bots now defaults to CQ Dry Run.')
4666 try:
4667 cl.SetCQState(_CQState.DRY_RUN)
4668 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4669 return 0
4670 except KeyboardInterrupt:
4671 raise
4672 except:
4673 print('WARNING: failed to trigger CQ Dry Run.\n'
4674 'Either:\n'
4675 ' * your project has no CQ\n'
4676 ' * you don\'t have permission to trigger Dry Run\n'
4677 ' * bug in this code (see stack trace below).\n'
4678 'Consider specifying which bots to trigger manually '
4679 'or asking your project owners for permissions '
4680 'or contacting Chrome Infrastructure team at '
4681 'https://www.chromium.org/infra\n\n')
4682 # Still raise exception so that stack trace is printed.
4683 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004684
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004685 for builders in masters.itervalues():
4686 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004687 print('ERROR You are trying to send a job to a triggered bot. This type '
4688 'of bot requires an\ninitial job from a parent (usually a builder).'
4689 ' Instead send your job to the parent.\n'
4690 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004691 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004692
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004693 patchset = cl.GetMostRecentPatchset()
4694 if patchset and patchset != cl.GetPatchset():
4695 print(
4696 '\nWARNING Mismatch between local config and server. Did a previous '
4697 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4698 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004699 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004700 try:
4701 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4702 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004703 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004704 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004705 except Exception as e:
4706 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004707 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4708 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004709 return 1
4710 else:
4711 try:
4712 cl.RpcServer().trigger_distributed_try_jobs(
4713 cl.GetIssue(), patchset, options.name, options.clobber,
4714 options.revision, masters)
4715 except urllib2.HTTPError as e:
4716 if e.code == 404:
4717 print('404 from rietveld; '
4718 'did you mean to use "git try" instead of "git cl try"?')
4719 return 1
4720 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004721
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004722 for (master, builders) in sorted(masters.iteritems()):
4723 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004724 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004725 length = max(len(builder) for builder in builders)
4726 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004727 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004728 return 0
4729
4730
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004731def CMDtry_results(parser, args):
4732 group = optparse.OptionGroup(parser, "Try job results options")
4733 group.add_option(
4734 "-p", "--patchset", type=int, help="patchset number if not current.")
4735 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004736 "--print-master", action='store_true', help="print master name as well.")
4737 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004738 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004739 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004740 group.add_option(
4741 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4742 help="Host of buildbucket. The default host is %default.")
4743 parser.add_option_group(group)
4744 auth.add_auth_options(parser)
4745 options, args = parser.parse_args(args)
4746 if args:
4747 parser.error('Unrecognized args: %s' % ' '.join(args))
4748
4749 auth_config = auth.extract_auth_config_from_options(options)
4750 cl = Changelist(auth_config=auth_config)
4751 if not cl.GetIssue():
4752 parser.error('Need to upload first')
4753
4754 if not options.patchset:
4755 options.patchset = cl.GetMostRecentPatchset()
4756 if options.patchset and options.patchset != cl.GetPatchset():
4757 print(
4758 '\nWARNING Mismatch between local config and server. Did a previous '
4759 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4760 'Continuing using\npatchset %s.\n' % options.patchset)
4761 try:
4762 jobs = fetch_try_jobs(auth_config, cl, options)
4763 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004764 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004765 return 1
4766 except Exception as e:
4767 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004768 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4769 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004770 return 1
4771 print_tryjobs(options, jobs)
4772 return 0
4773
4774
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004775@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004776def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004777 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004778 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004779 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004780 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004782 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004783 if args:
4784 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004785 branch = cl.GetBranch()
4786 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004787 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004788 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004789
4790 # Clear configured merge-base, if there is one.
4791 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004792 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004793 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004794 return 0
4795
4796
thestig@chromium.org00858c82013-12-02 23:08:03 +00004797def CMDweb(parser, args):
4798 """Opens the current CL in the web browser."""
4799 _, args = parser.parse_args(args)
4800 if args:
4801 parser.error('Unrecognized args: %s' % ' '.join(args))
4802
4803 issue_url = Changelist().GetIssueURL()
4804 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004805 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004806 return 1
4807
4808 webbrowser.open(issue_url)
4809 return 0
4810
4811
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004812def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004813 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004814 parser.add_option('-d', '--dry-run', action='store_true',
4815 help='trigger in dry run mode')
4816 parser.add_option('-c', '--clear', action='store_true',
4817 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004818 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004819 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004820 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004821 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004822 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004823 if args:
4824 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004825 if options.dry_run and options.clear:
4826 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4827
iannuccie53c9352016-08-17 14:40:40 -07004828 cl = Changelist(auth_config=auth_config, issue=options.issue,
4829 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004830 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004831 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004832 elif options.dry_run:
4833 state = _CQState.DRY_RUN
4834 else:
4835 state = _CQState.COMMIT
4836 if not cl.GetIssue():
4837 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004838 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004839 return 0
4840
4841
groby@chromium.org411034a2013-02-26 15:12:01 +00004842def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004843 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004844 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004845 auth.add_auth_options(parser)
4846 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004847 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004848 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004849 if args:
4850 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004851 cl = Changelist(auth_config=auth_config, issue=options.issue,
4852 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004853 # Ensure there actually is an issue to close.
4854 cl.GetDescription()
4855 cl.CloseIssue()
4856 return 0
4857
4858
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004859def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004860 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004861 auth.add_auth_options(parser)
4862 options, args = parser.parse_args(args)
4863 auth_config = auth.extract_auth_config_from_options(options)
4864 if args:
4865 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004866
4867 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004868 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004869 # Staged changes would be committed along with the patch from last
4870 # upload, hence counted toward the "last upload" side in the final
4871 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004872 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004873 return 1
4874
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004875 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004876 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004877 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004878 if not issue:
4879 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004880 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004881 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004882
4883 # Create a new branch based on the merge-base
4884 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004885 # Clear cached branch in cl object, to avoid overwriting original CL branch
4886 # properties.
4887 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004888 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004889 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004890 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004891 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004892 return rtn
4893
wychen@chromium.org06928532015-02-03 02:11:29 +00004894 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004895 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004896 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004897 finally:
4898 RunGit(['checkout', '-q', branch])
4899 RunGit(['branch', '-D', TMP_BRANCH])
4900
4901 return 0
4902
4903
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004904def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004905 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004906 parser.add_option(
4907 '--no-color',
4908 action='store_true',
4909 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004910 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004911 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004912 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004913
4914 author = RunGit(['config', 'user.email']).strip() or None
4915
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004916 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004917
4918 if args:
4919 if len(args) > 1:
4920 parser.error('Unknown args')
4921 base_branch = args[0]
4922 else:
4923 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004924 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004925
4926 change = cl.GetChange(base_branch, None)
4927 return owners_finder.OwnersFinder(
4928 [f.LocalPath() for f in
4929 cl.GetChange(base_branch, None).AffectedFiles()],
4930 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004931 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004932 disable_color=options.no_color).run()
4933
4934
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004935def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004936 """Generates a diff command."""
4937 # Generate diff for the current branch's changes.
4938 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4939 upstream_commit, '--' ]
4940
4941 if args:
4942 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004943 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004944 diff_cmd.append(arg)
4945 else:
4946 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004947
4948 return diff_cmd
4949
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004950def MatchingFileType(file_name, extensions):
4951 """Returns true if the file name ends with one of the given extensions."""
4952 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004953
enne@chromium.org555cfe42014-01-29 18:21:39 +00004954@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004955def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004956 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004957 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004958 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004959 parser.add_option('--full', action='store_true',
4960 help='Reformat the full content of all touched files')
4961 parser.add_option('--dry-run', action='store_true',
4962 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004963 parser.add_option('--python', action='store_true',
4964 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004965 parser.add_option('--diff', action='store_true',
4966 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004967 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004968
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004969 # git diff generates paths against the root of the repository. Change
4970 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004971 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004972 if rel_base_path:
4973 os.chdir(rel_base_path)
4974
digit@chromium.org29e47272013-05-17 17:01:46 +00004975 # Grab the merge-base commit, i.e. the upstream commit of the current
4976 # branch when it was created or the last time it was rebased. This is
4977 # to cover the case where the user may have called "git fetch origin",
4978 # moving the origin branch to a newer commit, but hasn't rebased yet.
4979 upstream_commit = None
4980 cl = Changelist()
4981 upstream_branch = cl.GetUpstreamBranch()
4982 if upstream_branch:
4983 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4984 upstream_commit = upstream_commit.strip()
4985
4986 if not upstream_commit:
4987 DieWithError('Could not find base commit for this branch. '
4988 'Are you in detached state?')
4989
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004990 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4991 diff_output = RunGit(changed_files_cmd)
4992 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004993 # Filter out files deleted by this CL
4994 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004995
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004996 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4997 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4998 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004999 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005000
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005001 top_dir = os.path.normpath(
5002 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5003
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005004 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5005 # formatted. This is used to block during the presubmit.
5006 return_value = 0
5007
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005008 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005009 # Locate the clang-format binary in the checkout
5010 try:
5011 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005012 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005013 DieWithError(e)
5014
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005015 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005016 cmd = [clang_format_tool]
5017 if not opts.dry_run and not opts.diff:
5018 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005019 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005020 if opts.diff:
5021 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005022 else:
5023 env = os.environ.copy()
5024 env['PATH'] = str(os.path.dirname(clang_format_tool))
5025 try:
5026 script = clang_format.FindClangFormatScriptInChromiumTree(
5027 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005028 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005029 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005030
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005031 cmd = [sys.executable, script, '-p0']
5032 if not opts.dry_run and not opts.diff:
5033 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005034
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005035 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5036 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005037
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005038 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5039 if opts.diff:
5040 sys.stdout.write(stdout)
5041 if opts.dry_run and len(stdout) > 0:
5042 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005043
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005044 # Similar code to above, but using yapf on .py files rather than clang-format
5045 # on C/C++ files
5046 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005047 yapf_tool = gclient_utils.FindExecutable('yapf')
5048 if yapf_tool is None:
5049 DieWithError('yapf not found in PATH')
5050
5051 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005052 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005053 cmd = [yapf_tool]
5054 if not opts.dry_run and not opts.diff:
5055 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005056 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005057 if opts.diff:
5058 sys.stdout.write(stdout)
5059 else:
5060 # TODO(sbc): yapf --lines mode still has some issues.
5061 # https://github.com/google/yapf/issues/154
5062 DieWithError('--python currently only works with --full')
5063
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005064 # Dart's formatter does not have the nice property of only operating on
5065 # modified chunks, so hard code full.
5066 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005067 try:
5068 command = [dart_format.FindDartFmtToolInChromiumTree()]
5069 if not opts.dry_run and not opts.diff:
5070 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005071 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005072
ppi@chromium.org6593d932016-03-03 15:41:15 +00005073 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005074 if opts.dry_run and stdout:
5075 return_value = 2
5076 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005077 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5078 'found in this checkout. Files in other languages are still '
5079 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005080
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005081 # Format GN build files. Always run on full build files for canonical form.
5082 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005083 cmd = ['gn', 'format' ]
5084 if opts.dry_run or opts.diff:
5085 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005086 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005087 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5088 shell=sys.platform == 'win32',
5089 cwd=top_dir)
5090 if opts.dry_run and gn_ret == 2:
5091 return_value = 2 # Not formatted.
5092 elif opts.diff and gn_ret == 2:
5093 # TODO this should compute and print the actual diff.
5094 print("This change has GN build file diff for " + gn_diff_file)
5095 elif gn_ret != 0:
5096 # For non-dry run cases (and non-2 return values for dry-run), a
5097 # nonzero error code indicates a failure, probably because the file
5098 # doesn't parse.
5099 DieWithError("gn format failed on " + gn_diff_file +
5100 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005101
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005102 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005103
5104
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005105@subcommand.usage('<codereview url or issue id>')
5106def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005107 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005108 _, args = parser.parse_args(args)
5109
5110 if len(args) != 1:
5111 parser.print_help()
5112 return 1
5113
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005114 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005115 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005116 parser.print_help()
5117 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005118 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005119
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005120 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005121 output = RunGit(['config', '--local', '--get-regexp',
5122 r'branch\..*\.%s' % issueprefix],
5123 error_ok=True)
5124 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005125 if issue == target_issue:
5126 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005127
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005128 branches = []
5129 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005130 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005131 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005132 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005133 return 1
5134 if len(branches) == 1:
5135 RunGit(['checkout', branches[0]])
5136 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005137 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005138 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005139 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005140 which = raw_input('Choose by index: ')
5141 try:
5142 RunGit(['checkout', branches[int(which)]])
5143 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005144 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005145 return 1
5146
5147 return 0
5148
5149
maruel@chromium.org29404b52014-09-08 22:58:00 +00005150def CMDlol(parser, args):
5151 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005152 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005153 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5154 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5155 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005156 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005157 return 0
5158
5159
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005160class OptionParser(optparse.OptionParser):
5161 """Creates the option parse and add --verbose support."""
5162 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005163 optparse.OptionParser.__init__(
5164 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005165 self.add_option(
5166 '-v', '--verbose', action='count', default=0,
5167 help='Use 2 times for more debugging info')
5168
5169 def parse_args(self, args=None, values=None):
5170 options, args = optparse.OptionParser.parse_args(self, args, values)
5171 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5172 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5173 return options, args
5174
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005175
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005176def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005177 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005178 print('\nYour python version %s is unsupported, please upgrade.\n' %
5179 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005180 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005181
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005182 # Reload settings.
5183 global settings
5184 settings = Settings()
5185
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005186 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005187 dispatcher = subcommand.CommandDispatcher(__name__)
5188 try:
5189 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005190 except auth.AuthenticationError as e:
5191 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005192 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005193 if e.code != 500:
5194 raise
5195 DieWithError(
5196 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5197 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005198 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005199
5200
5201if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005202 # These affect sys.stdout so do it outside of main() to simplify mocks in
5203 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005204 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005205 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005206 try:
5207 sys.exit(main(sys.argv[1:]))
5208 except KeyboardInterrupt:
5209 sys.stderr.write('interrupted\n')
5210 sys.exit(1)