blob: 9fabcc38beb35f4302f9f1be32569890189a4e94 [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."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000134 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
machenbach@chromium.org45453142015-09-15 08:45:22 +0000283def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000284 rietveld_url = settings.GetDefaultServerUrl()
285 rietveld_host = urlparse.urlparse(rietveld_url).hostname
286 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
287 http = authenticator.authorize(httplib2.Http())
288 http.force_exception_to_status_code = True
289 issue_props = changelist.GetIssueProperties()
290 issue = changelist.GetIssue()
291 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000292 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000293
294 buildbucket_put_url = (
295 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000296 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
298 hostname=rietveld_host,
299 issue=issue,
300 patch=patchset)
301
302 batch_req_body = {'builds': []}
303 print_text = []
304 print_text.append('Tried jobs on:')
305 for master, builders_and_tests in sorted(masters.iteritems()):
306 print_text.append('Master: %s' % master)
307 bucket = _prefix_master(master)
308 for builder, tests in sorted(builders_and_tests.iteritems()):
309 print_text.append(' %s: %s' % (builder, tests))
310 parameters = {
311 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000312 'changes': [{
313 'author': {'email': issue_props['owner_email']},
314 'revision': options.revision,
315 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 'properties': {
317 'category': category,
318 'issue': issue,
319 'master': master,
320 'patch_project': issue_props['project'],
321 'patch_storage': 'rietveld',
322 'patchset': patchset,
323 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000324 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000325 },
326 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000327 if 'presubmit' in builder.lower():
328 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000329 if tests:
330 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000331 if properties:
332 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000333 if options.clobber:
334 parameters['properties']['clobber'] = True
335 batch_req_body['builds'].append(
336 {
337 'bucket': bucket,
338 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000339 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000340 'tags': ['builder:%s' % builder,
341 'buildset:%s' % buildset,
342 'master:%s' % master,
343 'user_agent:git_cl_try']
344 }
345 )
346
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000347 _buildbucket_retry(
348 'triggering tryjobs',
349 http,
350 buildbucket_put_url,
351 'PUT',
352 body=json.dumps(batch_req_body),
353 headers={'Content-Type': 'application/json'}
354 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000355 print_text.append('To see results here, run: git cl try-results')
356 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700357 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000358
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000359
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360def fetch_try_jobs(auth_config, changelist, options):
361 """Fetches tryjobs from buildbucket.
362
363 Returns a map from build id to build info as json dictionary.
364 """
365 rietveld_url = settings.GetDefaultServerUrl()
366 rietveld_host = urlparse.urlparse(rietveld_url).hostname
367 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
368 if authenticator.has_cached_credentials():
369 http = authenticator.authorize(httplib2.Http())
370 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700371 print('Warning: Some results might be missing because %s' %
372 # Get the message on how to login.
373 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374 http = httplib2.Http()
375
376 http.force_exception_to_status_code = True
377
378 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
379 hostname=rietveld_host,
380 issue=changelist.GetIssue(),
381 patch=options.patchset)
382 params = {'tag': 'buildset:%s' % buildset}
383
384 builds = {}
385 while True:
386 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
387 hostname=options.buildbucket_host,
388 params=urllib.urlencode(params))
389 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
390 for build in content.get('builds', []):
391 builds[build['id']] = build
392 if 'next_cursor' in content:
393 params['start_cursor'] = content['next_cursor']
394 else:
395 break
396 return builds
397
398
399def print_tryjobs(options, builds):
400 """Prints nicely result of fetch_try_jobs."""
401 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700402 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 return
404
405 # Make a copy, because we'll be modifying builds dictionary.
406 builds = builds.copy()
407 builder_names_cache = {}
408
409 def get_builder(b):
410 try:
411 return builder_names_cache[b['id']]
412 except KeyError:
413 try:
414 parameters = json.loads(b['parameters_json'])
415 name = parameters['builder_name']
416 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700417 print('WARNING: failed to get builder name for build %s: %s' % (
418 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 name = None
420 builder_names_cache[b['id']] = name
421 return name
422
423 def get_bucket(b):
424 bucket = b['bucket']
425 if bucket.startswith('master.'):
426 return bucket[len('master.'):]
427 return bucket
428
429 if options.print_master:
430 name_fmt = '%%-%ds %%-%ds' % (
431 max(len(str(get_bucket(b))) for b in builds.itervalues()),
432 max(len(str(get_builder(b))) for b in builds.itervalues()))
433 def get_name(b):
434 return name_fmt % (get_bucket(b), get_builder(b))
435 else:
436 name_fmt = '%%-%ds' % (
437 max(len(str(get_builder(b))) for b in builds.itervalues()))
438 def get_name(b):
439 return name_fmt % get_builder(b)
440
441 def sort_key(b):
442 return b['status'], b.get('result'), get_name(b), b.get('url')
443
444 def pop(title, f, color=None, **kwargs):
445 """Pop matching builds from `builds` dict and print them."""
446
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000447 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000448 colorize = str
449 else:
450 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
451
452 result = []
453 for b in builds.values():
454 if all(b.get(k) == v for k, v in kwargs.iteritems()):
455 builds.pop(b['id'])
456 result.append(b)
457 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700458 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700460 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461
462 total = len(builds)
463 pop(status='COMPLETED', result='SUCCESS',
464 title='Successes:', color=Fore.GREEN,
465 f=lambda b: (get_name(b), b.get('url')))
466 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
467 title='Infra Failures:', color=Fore.MAGENTA,
468 f=lambda b: (get_name(b), b.get('url')))
469 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
470 title='Failures:', color=Fore.RED,
471 f=lambda b: (get_name(b), b.get('url')))
472 pop(status='COMPLETED', result='CANCELED',
473 title='Canceled:', color=Fore.MAGENTA,
474 f=lambda b: (get_name(b),))
475 pop(status='COMPLETED', result='FAILURE',
476 failure_reason='INVALID_BUILD_DEFINITION',
477 title='Wrong master/builder name:', color=Fore.MAGENTA,
478 f=lambda b: (get_name(b),))
479 pop(status='COMPLETED', result='FAILURE',
480 title='Other failures:',
481 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
482 pop(status='COMPLETED',
483 title='Other finished:',
484 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
485 pop(status='STARTED',
486 title='Started:', color=Fore.YELLOW,
487 f=lambda b: (get_name(b), b.get('url')))
488 pop(status='SCHEDULED',
489 title='Scheduled:',
490 f=lambda b: (get_name(b), 'id=%s' % b['id']))
491 # The last section is just in case buildbucket API changes OR there is a bug.
492 pop(title='Other:',
493 f=lambda b: (get_name(b), 'id=%s' % b['id']))
494 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700495 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000496
497
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000498def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
499 """Return the corresponding git ref if |base_url| together with |glob_spec|
500 matches the full |url|.
501
502 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
503 """
504 fetch_suburl, as_ref = glob_spec.split(':')
505 if allow_wildcards:
506 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
507 if glob_match:
508 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
509 # "branches/{472,597,648}/src:refs/remotes/svn/*".
510 branch_re = re.escape(base_url)
511 if glob_match.group(1):
512 branch_re += '/' + re.escape(glob_match.group(1))
513 wildcard = glob_match.group(2)
514 if wildcard == '*':
515 branch_re += '([^/]*)'
516 else:
517 # Escape and replace surrounding braces with parentheses and commas
518 # with pipe symbols.
519 wildcard = re.escape(wildcard)
520 wildcard = re.sub('^\\\\{', '(', wildcard)
521 wildcard = re.sub('\\\\,', '|', wildcard)
522 wildcard = re.sub('\\\\}$', ')', wildcard)
523 branch_re += wildcard
524 if glob_match.group(3):
525 branch_re += re.escape(glob_match.group(3))
526 match = re.match(branch_re, url)
527 if match:
528 return re.sub('\*$', match.group(1), as_ref)
529
530 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
531 if fetch_suburl:
532 full_url = base_url + '/' + fetch_suburl
533 else:
534 full_url = base_url
535 if full_url == url:
536 return as_ref
537 return None
538
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000539
iannucci@chromium.org79540052012-10-19 23:15:26 +0000540def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000541 """Prints statistics about the change to the user."""
542 # --no-ext-diff is broken in some versions of Git, so try to work around
543 # this by overriding the environment (but there is still a problem if the
544 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000545 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000546 if 'GIT_EXTERNAL_DIFF' in env:
547 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000548
549 if find_copies:
550 similarity_options = ['--find-copies-harder', '-l100000',
551 '-C%s' % similarity]
552 else:
553 similarity_options = ['-M%s' % similarity]
554
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000555 try:
556 stdout = sys.stdout.fileno()
557 except AttributeError:
558 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000559 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000560 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000561 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000562 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000563
564
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000565class BuildbucketResponseException(Exception):
566 pass
567
568
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569class Settings(object):
570 def __init__(self):
571 self.default_server = None
572 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000573 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574 self.is_git_svn = None
575 self.svn_branch = None
576 self.tree_status_url = None
577 self.viewvc_url = None
578 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000579 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000580 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000581 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000582 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000583 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000584 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000585 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586
587 def LazyUpdateIfNeeded(self):
588 """Updates the settings from a codereview.settings file, if available."""
589 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000590 # The only value that actually changes the behavior is
591 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000592 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000593 error_ok=True
594 ).strip().lower()
595
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000597 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598 LoadCodereviewSettingsFromFile(cr_settings_file)
599 self.updated = True
600
601 def GetDefaultServerUrl(self, error_ok=False):
602 if not self.default_server:
603 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000604 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000605 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 if error_ok:
607 return self.default_server
608 if not self.default_server:
609 error_message = ('Could not find settings file. You must configure '
610 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000611 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000612 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 return self.default_server
614
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000615 @staticmethod
616 def GetRelativeRoot():
617 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000620 if self.root is None:
621 self.root = os.path.abspath(self.GetRelativeRoot())
622 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000624 def GetGitMirror(self, remote='origin'):
625 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000626 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000627 if not os.path.isdir(local_url):
628 return None
629 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
630 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
631 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
632 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
633 if mirror.exists():
634 return mirror
635 return None
636
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 def GetIsGitSvn(self):
638 """Return true if this repo looks like it's using git-svn."""
639 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000640 if self.GetPendingRefPrefix():
641 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
642 self.is_git_svn = False
643 else:
644 # If you have any "svn-remote.*" config keys, we think you're using svn.
645 self.is_git_svn = RunGitWithCode(
646 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 return self.is_git_svn
648
649 def GetSVNBranch(self):
650 if self.svn_branch is None:
651 if not self.GetIsGitSvn():
652 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
653
654 # Try to figure out which remote branch we're based on.
655 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000656 # 1) iterate through our branch history and find the svn URL.
657 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658
659 # regexp matching the git-svn line that contains the URL.
660 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
661
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000662 # We don't want to go through all of history, so read a line from the
663 # pipe at a time.
664 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000665 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000666 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
667 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000668 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000669 for line in proc.stdout:
670 match = git_svn_re.match(line)
671 if match:
672 url = match.group(1)
673 proc.stdout.close() # Cut pipe.
674 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000676 if url:
677 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
678 remotes = RunGit(['config', '--get-regexp',
679 r'^svn-remote\..*\.url']).splitlines()
680 for remote in remotes:
681 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000682 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000683 remote = match.group(1)
684 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000685 rewrite_root = RunGit(
686 ['config', 'svn-remote.%s.rewriteRoot' % remote],
687 error_ok=True).strip()
688 if rewrite_root:
689 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000690 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000691 ['config', 'svn-remote.%s.fetch' % remote],
692 error_ok=True).strip()
693 if fetch_spec:
694 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
695 if self.svn_branch:
696 break
697 branch_spec = RunGit(
698 ['config', 'svn-remote.%s.branches' % remote],
699 error_ok=True).strip()
700 if branch_spec:
701 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
702 if self.svn_branch:
703 break
704 tag_spec = RunGit(
705 ['config', 'svn-remote.%s.tags' % remote],
706 error_ok=True).strip()
707 if tag_spec:
708 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
709 if self.svn_branch:
710 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
712 if not self.svn_branch:
713 DieWithError('Can\'t guess svn branch -- try specifying it on the '
714 'command line')
715
716 return self.svn_branch
717
718 def GetTreeStatusUrl(self, error_ok=False):
719 if not self.tree_status_url:
720 error_message = ('You must configure your tree status URL by running '
721 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000722 self.tree_status_url = self._GetRietveldConfig(
723 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 return self.tree_status_url
725
726 def GetViewVCUrl(self):
727 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000728 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 return self.viewvc_url
730
rmistry@google.com90752582014-01-14 21:04:50 +0000731 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000733
rmistry@google.com78948ed2015-07-08 23:09:57 +0000734 def GetIsSkipDependencyUpload(self, branch_name):
735 """Returns true if specified branch should skip dep uploads."""
736 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
737 error_ok=True)
738
rmistry@google.com5626a922015-02-26 14:03:30 +0000739 def GetRunPostUploadHook(self):
740 run_post_upload_hook = self._GetRietveldConfig(
741 'run-post-upload-hook', error_ok=True)
742 return run_post_upload_hook == "True"
743
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000744 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000745 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000746
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000747 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000748 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000749
ukai@chromium.orge8077812012-02-03 03:41:46 +0000750 def GetIsGerrit(self):
751 """Return true if this repo is assosiated with gerrit code review system."""
752 if self.is_gerrit is None:
753 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
754 return self.is_gerrit
755
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000756 def GetSquashGerritUploads(self):
757 """Return true if uploads to Gerrit should be squashed by default."""
758 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700759 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
760 if self.squash_gerrit_uploads is None:
761 # Default is squash now (http://crbug.com/611892#c23).
762 self.squash_gerrit_uploads = not (
763 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
764 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000765 return self.squash_gerrit_uploads
766
tandriia60502f2016-06-20 02:01:53 -0700767 def GetSquashGerritUploadsOverride(self):
768 """Return True or False if codereview.settings should be overridden.
769
770 Returns None if no override has been defined.
771 """
772 # See also http://crbug.com/611892#c23
773 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
774 error_ok=True).strip()
775 if result == 'true':
776 return True
777 if result == 'false':
778 return False
779 return None
780
tandrii@chromium.org28253532016-04-14 13:46:56 +0000781 def GetGerritSkipEnsureAuthenticated(self):
782 """Return True if EnsureAuthenticated should not be done for Gerrit
783 uploads."""
784 if self.gerrit_skip_ensure_authenticated is None:
785 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000786 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000787 error_ok=True).strip() == 'true')
788 return self.gerrit_skip_ensure_authenticated
789
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000790 def GetGitEditor(self):
791 """Return the editor specified in the git config, or None if none is."""
792 if self.git_editor is None:
793 self.git_editor = self._GetConfig('core.editor', error_ok=True)
794 return self.git_editor or None
795
thestig@chromium.org44202a22014-03-11 19:22:18 +0000796 def GetLintRegex(self):
797 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
798 DEFAULT_LINT_REGEX)
799
800 def GetLintIgnoreRegex(self):
801 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
802 DEFAULT_LINT_IGNORE_REGEX)
803
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000804 def GetProject(self):
805 if not self.project:
806 self.project = self._GetRietveldConfig('project', error_ok=True)
807 return self.project
808
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000809 def GetForceHttpsCommitUrl(self):
810 if not self.force_https_commit_url:
811 self.force_https_commit_url = self._GetRietveldConfig(
812 'force-https-commit-url', error_ok=True)
813 return self.force_https_commit_url
814
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000815 def GetPendingRefPrefix(self):
816 if not self.pending_ref_prefix:
817 self.pending_ref_prefix = self._GetRietveldConfig(
818 'pending-ref-prefix', error_ok=True)
819 return self.pending_ref_prefix
820
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000821 def _GetRietveldConfig(self, param, **kwargs):
822 return self._GetConfig('rietveld.' + param, **kwargs)
823
rmistry@google.com78948ed2015-07-08 23:09:57 +0000824 def _GetBranchConfig(self, branch_name, param, **kwargs):
825 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 def _GetConfig(self, param, **kwargs):
828 self.LazyUpdateIfNeeded()
829 return RunGit(['config', param], **kwargs).strip()
830
831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832def ShortBranchName(branch):
833 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000834 return branch.replace('refs/heads/', '', 1)
835
836
837def GetCurrentBranchRef():
838 """Returns branch ref (e.g., refs/heads/master) or None."""
839 return RunGit(['symbolic-ref', 'HEAD'],
840 stderr=subprocess2.VOID, error_ok=True).strip() or None
841
842
843def GetCurrentBranch():
844 """Returns current branch or None.
845
846 For refs/heads/* branches, returns just last part. For others, full ref.
847 """
848 branchref = GetCurrentBranchRef()
849 if branchref:
850 return ShortBranchName(branchref)
851 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
853
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000854class _CQState(object):
855 """Enum for states of CL with respect to Commit Queue."""
856 NONE = 'none'
857 DRY_RUN = 'dry_run'
858 COMMIT = 'commit'
859
860 ALL_STATES = [NONE, DRY_RUN, COMMIT]
861
862
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000863class _ParsedIssueNumberArgument(object):
864 def __init__(self, issue=None, patchset=None, hostname=None):
865 self.issue = issue
866 self.patchset = patchset
867 self.hostname = hostname
868
869 @property
870 def valid(self):
871 return self.issue is not None
872
873
874class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
875 def __init__(self, *args, **kwargs):
876 self.patch_url = kwargs.pop('patch_url', None)
877 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
878
879
880def ParseIssueNumberArgument(arg):
881 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
882 fail_result = _ParsedIssueNumberArgument()
883
884 if arg.isdigit():
885 return _ParsedIssueNumberArgument(issue=int(arg))
886 if not arg.startswith('http'):
887 return fail_result
888 url = gclient_utils.UpgradeToHttps(arg)
889 try:
890 parsed_url = urlparse.urlparse(url)
891 except ValueError:
892 return fail_result
893 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
894 tmp = cls.ParseIssueURL(parsed_url)
895 if tmp is not None:
896 return tmp
897 return fail_result
898
899
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901 """Changelist works with one changelist in local branch.
902
903 Supports two codereview backends: Rietveld or Gerrit, selected at object
904 creation.
905
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000906 Notes:
907 * Not safe for concurrent multi-{thread,process} use.
908 * Caches values from current branch. Therefore, re-use after branch change
909 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000910 """
911
912 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
913 """Create a new ChangeList instance.
914
915 If issue is given, the codereview must be given too.
916
917 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
918 Otherwise, it's decided based on current configuration of the local branch,
919 with default being 'rietveld' for backwards compatibility.
920 See _load_codereview_impl for more details.
921
922 **kwargs will be passed directly to codereview implementation.
923 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000925 global settings
926 if not settings:
927 # Happens when git_cl.py is used as a utility library.
928 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000929
930 if issue:
931 assert codereview, 'codereview must be known, if issue is known'
932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933 self.branchref = branchref
934 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000935 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 self.branch = ShortBranchName(self.branchref)
937 else:
938 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000940 self.lookedup_issue = False
941 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942 self.has_description = False
943 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000944 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000946 self.cc = None
947 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000948 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000949
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000950 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000951 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000952 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000953 assert self._codereview_impl
954 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000955
956 def _load_codereview_impl(self, codereview=None, **kwargs):
957 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000958 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
959 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
960 self._codereview = codereview
961 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000962 return
963
964 # Automatic selection based on issue number set for a current branch.
965 # Rietveld takes precedence over Gerrit.
966 assert not self.issue
967 # Whether we find issue or not, we are doing the lookup.
968 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000969 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970 setting = cls.IssueSetting(self.GetBranch())
971 issue = RunGit(['config', setting], error_ok=True).strip()
972 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000973 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000974 self._codereview_impl = cls(self, **kwargs)
975 self.issue = int(issue)
976 return
977
978 # No issue is set for this branch, so decide based on repo-wide settings.
979 return self._load_codereview_impl(
980 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
981 **kwargs)
982
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000983 def IsGerrit(self):
984 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000985
986 def GetCCList(self):
987 """Return the users cc'd on this CL.
988
989 Return is a string suitable for passing to gcl with the --cc flag.
990 """
991 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000992 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000993 more_cc = ','.join(self.watchers)
994 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
995 return self.cc
996
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000997 def GetCCListWithoutDefault(self):
998 """Return the users cc'd on this CL excluding default ones."""
999 if self.cc is None:
1000 self.cc = ','.join(self.watchers)
1001 return self.cc
1002
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001003 def SetWatchers(self, watchers):
1004 """Set the list of email addresses that should be cc'd based on the changed
1005 files in this CL.
1006 """
1007 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008
1009 def GetBranch(self):
1010 """Returns the short branch name, e.g. 'master'."""
1011 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001012 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001013 if not branchref:
1014 return None
1015 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016 self.branch = ShortBranchName(self.branchref)
1017 return self.branch
1018
1019 def GetBranchRef(self):
1020 """Returns the full branch name, e.g. 'refs/heads/master'."""
1021 self.GetBranch() # Poke the lazy loader.
1022 return self.branchref
1023
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001024 def ClearBranch(self):
1025 """Clears cached branch data of this object."""
1026 self.branch = self.branchref = None
1027
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001028 @staticmethod
1029 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001030 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 e.g. 'origin', 'refs/heads/master'
1032 """
1033 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1035 error_ok=True).strip()
1036 if upstream_branch:
1037 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1038 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001039 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1040 error_ok=True).strip()
1041 if upstream_branch:
1042 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001044 # Fall back on trying a git-svn upstream branch.
1045 if settings.GetIsGitSvn():
1046 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001048 # Else, try to guess the origin remote.
1049 remote_branches = RunGit(['branch', '-r']).split()
1050 if 'origin/master' in remote_branches:
1051 # Fall back on origin/master if it exits.
1052 remote = 'origin'
1053 upstream_branch = 'refs/heads/master'
1054 elif 'origin/trunk' in remote_branches:
1055 # Fall back on origin/trunk if it exists. Generally a shared
1056 # git-svn clone
1057 remote = 'origin'
1058 upstream_branch = 'refs/heads/trunk'
1059 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001060 DieWithError(
1061 'Unable to determine default branch to diff against.\n'
1062 'Either pass complete "git diff"-style arguments, like\n'
1063 ' git cl upload origin/master\n'
1064 'or verify this branch is set up to track another \n'
1065 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066
1067 return remote, upstream_branch
1068
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001069 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001070 upstream_branch = self.GetUpstreamBranch()
1071 if not BranchExists(upstream_branch):
1072 DieWithError('The upstream for the current branch (%s) does not exist '
1073 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001074 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001075 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 def GetUpstreamBranch(self):
1078 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001079 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001081 upstream_branch = upstream_branch.replace('refs/heads/',
1082 'refs/remotes/%s/' % remote)
1083 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1084 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 self.upstream_branch = upstream_branch
1086 return self.upstream_branch
1087
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001088 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001089 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001090 remote, branch = None, self.GetBranch()
1091 seen_branches = set()
1092 while branch not in seen_branches:
1093 seen_branches.add(branch)
1094 remote, branch = self.FetchUpstreamTuple(branch)
1095 branch = ShortBranchName(branch)
1096 if remote != '.' or branch.startswith('refs/remotes'):
1097 break
1098 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001099 remotes = RunGit(['remote'], error_ok=True).split()
1100 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001101 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001102 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001104 logging.warning('Could not determine which remote this change is '
1105 'associated with, so defaulting to "%s". This may '
1106 'not be what you want. You may prevent this message '
1107 'by running "git svn info" as documented here: %s',
1108 self._remote,
1109 GIT_INSTRUCTIONS_URL)
1110 else:
1111 logging.warn('Could not determine which remote this change is '
1112 'associated with. You may prevent this message by '
1113 'running "git svn info" as documented here: %s',
1114 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 branch = 'HEAD'
1116 if branch.startswith('refs/remotes'):
1117 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001118 elif branch.startswith('refs/branch-heads/'):
1119 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001120 else:
1121 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001122 return self._remote
1123
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001124 def GitSanityChecks(self, upstream_git_obj):
1125 """Checks git repo status and ensures diff is from local commits."""
1126
sbc@chromium.org79706062015-01-14 21:18:12 +00001127 if upstream_git_obj is None:
1128 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001129 print('ERROR: unable to determine current branch (detached HEAD?)',
1130 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001131 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001132 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001133 return False
1134
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001135 # Verify the commit we're diffing against is in our current branch.
1136 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1137 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1138 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001139 print('ERROR: %s is not in the current branch. You may need to rebase '
1140 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001141 return False
1142
1143 # List the commits inside the diff, and verify they are all local.
1144 commits_in_diff = RunGit(
1145 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1146 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1147 remote_branch = remote_branch.strip()
1148 if code != 0:
1149 _, remote_branch = self.GetRemoteBranch()
1150
1151 commits_in_remote = RunGit(
1152 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1153
1154 common_commits = set(commits_in_diff) & set(commits_in_remote)
1155 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001156 print('ERROR: Your diff contains %d commits already in %s.\n'
1157 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1158 'the diff. If you are using a custom git flow, you can override'
1159 ' the reference used for this check with "git config '
1160 'gitcl.remotebranch <git-ref>".' % (
1161 len(common_commits), remote_branch, upstream_git_obj),
1162 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001163 return False
1164 return True
1165
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001166 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001167 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001168
1169 Returns None if it is not set.
1170 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001171 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1172 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001173
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001174 def GetGitSvnRemoteUrl(self):
1175 """Return the configured git-svn remote URL parsed from git svn info.
1176
1177 Returns None if it is not set.
1178 """
1179 # URL is dependent on the current directory.
1180 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1181 if data:
1182 keys = dict(line.split(': ', 1) for line in data.splitlines()
1183 if ': ' in line)
1184 return keys.get('URL', None)
1185 return None
1186
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 def GetRemoteUrl(self):
1188 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1189
1190 Returns None if there is no remote.
1191 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001192 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001193 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1194
1195 # If URL is pointing to a local directory, it is probably a git cache.
1196 if os.path.isdir(url):
1197 url = RunGit(['config', 'remote.%s.url' % remote],
1198 error_ok=True,
1199 cwd=url).strip()
1200 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001202 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001203 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001204 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001205 issue = RunGit(['config',
1206 self._codereview_impl.IssueSetting(self.GetBranch())],
1207 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 self.issue = int(issue) or None if issue else None
1209 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return self.issue
1211
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 def GetIssueURL(self):
1213 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 issue = self.GetIssue()
1215 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001216 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001217 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
1219 def GetDescription(self, pretty=False):
1220 if not self.has_description:
1221 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001222 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 self.has_description = True
1224 if pretty:
1225 wrapper = textwrap.TextWrapper()
1226 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1227 return wrapper.fill(self.description)
1228 return self.description
1229
1230 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001231 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001233 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001235 self.patchset = int(patchset) or None if patchset else None
1236 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 return self.patchset
1238
1239 def SetPatchset(self, patchset):
1240 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001243 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001244 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001246 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001247 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001248 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001250 def SetIssue(self, issue=None):
1251 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001252 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1253 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001255 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001256 RunGit(['config', issue_setting, str(issue)])
1257 codereview_server = self._codereview_impl.GetCodereviewServer()
1258 if codereview_server:
1259 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001261 # Reset it regardless. It doesn't hurt.
1262 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1263 for prop in (['last-upload-hash'] +
1264 self._codereview_impl._PostUnsetIssueProperties()):
1265 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1266 for setting in config_settings:
1267 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001268 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001269 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001271 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001272 if not self.GitSanityChecks(upstream_branch):
1273 DieWithError('\nGit sanity check failure')
1274
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001275 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001276 if not root:
1277 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001278 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001279
1280 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001281 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001282 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001283 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001284 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001285 except subprocess2.CalledProcessError:
1286 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001287 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001288 'This branch probably doesn\'t exist anymore. To reset the\n'
1289 'tracking branch, please run\n'
1290 ' git branch --set-upstream %s trunk\n'
1291 'replacing trunk with origin/master or the relevant branch') %
1292 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001293
maruel@chromium.org52424302012-08-29 15:14:30 +00001294 issue = self.GetIssue()
1295 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001296 if issue:
1297 description = self.GetDescription()
1298 else:
1299 # If the change was never uploaded, use the log messages of all commits
1300 # up to the branch point, as git cl upload will prefill the description
1301 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001302 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1303 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001304
1305 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001306 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001307 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001308 name,
1309 description,
1310 absroot,
1311 files,
1312 issue,
1313 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001314 author,
1315 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001316
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001317 def UpdateDescription(self, description):
1318 self.description = description
1319 return self._codereview_impl.UpdateDescriptionRemote(description)
1320
1321 def RunHook(self, committing, may_prompt, verbose, change):
1322 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1323 try:
1324 return presubmit_support.DoPresubmitChecks(change, committing,
1325 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1326 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001327 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1328 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001329 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001330 DieWithError(
1331 ('%s\nMaybe your depot_tools is out of date?\n'
1332 'If all fails, contact maruel@') % e)
1333
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001334 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1335 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001336 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1337 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001338 else:
1339 # Assume url.
1340 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1341 urlparse.urlparse(issue_arg))
1342 if not parsed_issue_arg or not parsed_issue_arg.valid:
1343 DieWithError('Failed to parse issue argument "%s". '
1344 'Must be an issue number or a valid URL.' % issue_arg)
1345 return self._codereview_impl.CMDPatchWithParsedIssue(
1346 parsed_issue_arg, reject, nocommit, directory)
1347
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001348 def CMDUpload(self, options, git_diff_args, orig_args):
1349 """Uploads a change to codereview."""
1350 if git_diff_args:
1351 # TODO(ukai): is it ok for gerrit case?
1352 base_branch = git_diff_args[0]
1353 else:
1354 if self.GetBranch() is None:
1355 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1356
1357 # Default to diffing against common ancestor of upstream branch
1358 base_branch = self.GetCommonAncestorWithUpstream()
1359 git_diff_args = [base_branch, 'HEAD']
1360
1361 # Make sure authenticated to codereview before running potentially expensive
1362 # hooks. It is a fast, best efforts check. Codereview still can reject the
1363 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001364 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001365
1366 # Apply watchlists on upload.
1367 change = self.GetChange(base_branch, None)
1368 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1369 files = [f.LocalPath() for f in change.AffectedFiles()]
1370 if not options.bypass_watchlists:
1371 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1372
1373 if not options.bypass_hooks:
1374 if options.reviewers or options.tbr_owners:
1375 # Set the reviewer list now so that presubmit checks can access it.
1376 change_description = ChangeDescription(change.FullDescriptionText())
1377 change_description.update_reviewers(options.reviewers,
1378 options.tbr_owners,
1379 change)
1380 change.SetDescriptionText(change_description.description)
1381 hook_results = self.RunHook(committing=False,
1382 may_prompt=not options.force,
1383 verbose=options.verbose,
1384 change=change)
1385 if not hook_results.should_continue():
1386 return 1
1387 if not options.reviewers and hook_results.reviewers:
1388 options.reviewers = hook_results.reviewers.split(',')
1389
1390 if self.GetIssue():
1391 latest_patchset = self.GetMostRecentPatchset()
1392 local_patchset = self.GetPatchset()
1393 if (latest_patchset and local_patchset and
1394 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001395 print('The last upload made from this repository was patchset #%d but '
1396 'the most recent patchset on the server is #%d.'
1397 % (local_patchset, latest_patchset))
1398 print('Uploading will still work, but if you\'ve uploaded to this '
1399 'issue from another machine or branch the patch you\'re '
1400 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001401 ask_for_data('About to upload; enter to confirm.')
1402
1403 print_stats(options.similarity, options.find_copies, git_diff_args)
1404 ret = self.CMDUploadChange(options, git_diff_args, change)
1405 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001406 if options.use_commit_queue:
1407 self.SetCQState(_CQState.COMMIT)
1408 elif options.cq_dry_run:
1409 self.SetCQState(_CQState.DRY_RUN)
1410
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001411 git_set_branch_value('last-upload-hash',
1412 RunGit(['rev-parse', 'HEAD']).strip())
1413 # Run post upload hooks, if specified.
1414 if settings.GetRunPostUploadHook():
1415 presubmit_support.DoPostUploadExecuter(
1416 change,
1417 self,
1418 settings.GetRoot(),
1419 options.verbose,
1420 sys.stdout)
1421
1422 # Upload all dependencies if specified.
1423 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001424 print()
1425 print('--dependencies has been specified.')
1426 print('All dependent local branches will be re-uploaded.')
1427 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001428 # Remove the dependencies flag from args so that we do not end up in a
1429 # loop.
1430 orig_args.remove('--dependencies')
1431 ret = upload_branch_deps(self, orig_args)
1432 return ret
1433
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001434 def SetCQState(self, new_state):
1435 """Update the CQ state for latest patchset.
1436
1437 Issue must have been already uploaded and known.
1438 """
1439 assert new_state in _CQState.ALL_STATES
1440 assert self.GetIssue()
1441 return self._codereview_impl.SetCQState(new_state)
1442
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001443 # Forward methods to codereview specific implementation.
1444
1445 def CloseIssue(self):
1446 return self._codereview_impl.CloseIssue()
1447
1448 def GetStatus(self):
1449 return self._codereview_impl.GetStatus()
1450
1451 def GetCodereviewServer(self):
1452 return self._codereview_impl.GetCodereviewServer()
1453
1454 def GetApprovingReviewers(self):
1455 return self._codereview_impl.GetApprovingReviewers()
1456
1457 def GetMostRecentPatchset(self):
1458 return self._codereview_impl.GetMostRecentPatchset()
1459
1460 def __getattr__(self, attr):
1461 # This is because lots of untested code accesses Rietveld-specific stuff
1462 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001463 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001464 return getattr(self._codereview_impl, attr)
1465
1466
1467class _ChangelistCodereviewBase(object):
1468 """Abstract base class encapsulating codereview specifics of a changelist."""
1469 def __init__(self, changelist):
1470 self._changelist = changelist # instance of Changelist
1471
1472 def __getattr__(self, attr):
1473 # Forward methods to changelist.
1474 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1475 # _RietveldChangelistImpl to avoid this hack?
1476 return getattr(self._changelist, attr)
1477
1478 def GetStatus(self):
1479 """Apply a rough heuristic to give a simple summary of an issue's review
1480 or CQ status, assuming adherence to a common workflow.
1481
1482 Returns None if no issue for this branch, or specific string keywords.
1483 """
1484 raise NotImplementedError()
1485
1486 def GetCodereviewServer(self):
1487 """Returns server URL without end slash, like "https://codereview.com"."""
1488 raise NotImplementedError()
1489
1490 def FetchDescription(self):
1491 """Fetches and returns description from the codereview server."""
1492 raise NotImplementedError()
1493
1494 def GetCodereviewServerSetting(self):
1495 """Returns git config setting for the codereview server."""
1496 raise NotImplementedError()
1497
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001498 @classmethod
1499 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001500 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001501
1502 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001503 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001504 """Returns name of git config setting which stores issue number for a given
1505 branch."""
1506 raise NotImplementedError()
1507
1508 def PatchsetSetting(self):
1509 """Returns name of git config setting which stores issue number."""
1510 raise NotImplementedError()
1511
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001512 def _PostUnsetIssueProperties(self):
1513 """Which branch-specific properties to erase when unsettin issue."""
1514 raise NotImplementedError()
1515
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001516 def GetRieveldObjForPresubmit(self):
1517 # This is an unfortunate Rietveld-embeddedness in presubmit.
1518 # For non-Rietveld codereviews, this probably should return a dummy object.
1519 raise NotImplementedError()
1520
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001521 def GetGerritObjForPresubmit(self):
1522 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1523 return None
1524
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001525 def UpdateDescriptionRemote(self, description):
1526 """Update the description on codereview site."""
1527 raise NotImplementedError()
1528
1529 def CloseIssue(self):
1530 """Closes the issue."""
1531 raise NotImplementedError()
1532
1533 def GetApprovingReviewers(self):
1534 """Returns a list of reviewers approving the change.
1535
1536 Note: not necessarily committers.
1537 """
1538 raise NotImplementedError()
1539
1540 def GetMostRecentPatchset(self):
1541 """Returns the most recent patchset number from the codereview site."""
1542 raise NotImplementedError()
1543
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001544 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1545 directory):
1546 """Fetches and applies the issue.
1547
1548 Arguments:
1549 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1550 reject: if True, reject the failed patch instead of switching to 3-way
1551 merge. Rietveld only.
1552 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1553 only.
1554 directory: switch to directory before applying the patch. Rietveld only.
1555 """
1556 raise NotImplementedError()
1557
1558 @staticmethod
1559 def ParseIssueURL(parsed_url):
1560 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1561 failed."""
1562 raise NotImplementedError()
1563
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001564 def EnsureAuthenticated(self, force):
1565 """Best effort check that user is authenticated with codereview server.
1566
1567 Arguments:
1568 force: whether to skip confirmation questions.
1569 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001570 raise NotImplementedError()
1571
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001572 def CMDUploadChange(self, options, args, change):
1573 """Uploads a change to codereview."""
1574 raise NotImplementedError()
1575
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001576 def SetCQState(self, new_state):
1577 """Update the CQ state for latest patchset.
1578
1579 Issue must have been already uploaded and known.
1580 """
1581 raise NotImplementedError()
1582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583
1584class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1585 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1586 super(_RietveldChangelistImpl, self).__init__(changelist)
1587 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001588 if not rietveld_server:
1589 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590
1591 self._rietveld_server = rietveld_server
1592 self._auth_config = auth_config
1593 self._props = None
1594 self._rpc_server = None
1595
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001596 def GetCodereviewServer(self):
1597 if not self._rietveld_server:
1598 # If we're on a branch then get the server potentially associated
1599 # with that branch.
1600 if self.GetIssue():
1601 rietveld_server_setting = self.GetCodereviewServerSetting()
1602 if rietveld_server_setting:
1603 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1604 ['config', rietveld_server_setting], error_ok=True).strip())
1605 if not self._rietveld_server:
1606 self._rietveld_server = settings.GetDefaultServerUrl()
1607 return self._rietveld_server
1608
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001609 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610 """Best effort check that user is authenticated with Rietveld server."""
1611 if self._auth_config.use_oauth2:
1612 authenticator = auth.get_authenticator_for_host(
1613 self.GetCodereviewServer(), self._auth_config)
1614 if not authenticator.has_cached_credentials():
1615 raise auth.LoginRequiredError(self.GetCodereviewServer())
1616
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001617 def FetchDescription(self):
1618 issue = self.GetIssue()
1619 assert issue
1620 try:
1621 return self.RpcServer().get_description(issue).strip()
1622 except urllib2.HTTPError as e:
1623 if e.code == 404:
1624 DieWithError(
1625 ('\nWhile fetching the description for issue %d, received a '
1626 '404 (not found)\n'
1627 'error. It is likely that you deleted this '
1628 'issue on the server. If this is the\n'
1629 'case, please run\n\n'
1630 ' git cl issue 0\n\n'
1631 'to clear the association with the deleted issue. Then run '
1632 'this command again.') % issue)
1633 else:
1634 DieWithError(
1635 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1636 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001637 print('Warning: Failed to retrieve CL description due to network '
1638 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001639 return ''
1640
1641 def GetMostRecentPatchset(self):
1642 return self.GetIssueProperties()['patchsets'][-1]
1643
1644 def GetPatchSetDiff(self, issue, patchset):
1645 return self.RpcServer().get(
1646 '/download/issue%s_%s.diff' % (issue, patchset))
1647
1648 def GetIssueProperties(self):
1649 if self._props is None:
1650 issue = self.GetIssue()
1651 if not issue:
1652 self._props = {}
1653 else:
1654 self._props = self.RpcServer().get_issue_properties(issue, True)
1655 return self._props
1656
1657 def GetApprovingReviewers(self):
1658 return get_approving_reviewers(self.GetIssueProperties())
1659
1660 def AddComment(self, message):
1661 return self.RpcServer().add_comment(self.GetIssue(), message)
1662
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001663 def GetStatus(self):
1664 """Apply a rough heuristic to give a simple summary of an issue's review
1665 or CQ status, assuming adherence to a common workflow.
1666
1667 Returns None if no issue for this branch, or one of the following keywords:
1668 * 'error' - error from review tool (including deleted issues)
1669 * 'unsent' - not sent for review
1670 * 'waiting' - waiting for review
1671 * 'reply' - waiting for owner to reply to review
1672 * 'lgtm' - LGTM from at least one approved reviewer
1673 * 'commit' - in the commit queue
1674 * 'closed' - closed
1675 """
1676 if not self.GetIssue():
1677 return None
1678
1679 try:
1680 props = self.GetIssueProperties()
1681 except urllib2.HTTPError:
1682 return 'error'
1683
1684 if props.get('closed'):
1685 # Issue is closed.
1686 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001687 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001688 # Issue is in the commit queue.
1689 return 'commit'
1690
1691 try:
1692 reviewers = self.GetApprovingReviewers()
1693 except urllib2.HTTPError:
1694 return 'error'
1695
1696 if reviewers:
1697 # Was LGTM'ed.
1698 return 'lgtm'
1699
1700 messages = props.get('messages') or []
1701
tandrii9d2c7a32016-06-22 03:42:45 -07001702 # Skip CQ messages that don't require owner's action.
1703 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1704 if 'Dry run:' in messages[-1]['text']:
1705 messages.pop()
1706 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1707 # This message always follows prior messages from CQ,
1708 # so skip this too.
1709 messages.pop()
1710 else:
1711 # This is probably a CQ messages warranting user attention.
1712 break
1713
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001714 if not messages:
1715 # No message was sent.
1716 return 'unsent'
1717 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001718 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001719 return 'reply'
1720 return 'waiting'
1721
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001722 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001723 return self.RpcServer().update_description(
1724 self.GetIssue(), self.description)
1725
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001727 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001729 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001730 return self.SetFlags({flag: value})
1731
1732 def SetFlags(self, flags):
1733 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001734 """
phajdan.jr68598232016-08-10 03:28:28 -07001735 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001736 try:
tandrii4b233bd2016-07-06 03:50:29 -07001737 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001738 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001739 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001740 if e.code == 404:
1741 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1742 if e.code == 403:
1743 DieWithError(
1744 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001745 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001746 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001747
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001748 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749 """Returns an upload.RpcServer() to access this review's rietveld instance.
1750 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001751 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001752 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001754 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001755 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001757 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001758 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001759 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001761 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762 """Return the git setting that stores this change's most recent patchset."""
1763 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1764
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001767 branch = self.GetBranch()
1768 if branch:
1769 return 'branch.%s.rietveldserver' % branch
1770 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001772 def _PostUnsetIssueProperties(self):
1773 """Which branch-specific properties to erase when unsetting issue."""
1774 return ['rietveldserver']
1775
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776 def GetRieveldObjForPresubmit(self):
1777 return self.RpcServer()
1778
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001779 def SetCQState(self, new_state):
1780 props = self.GetIssueProperties()
1781 if props.get('private'):
1782 DieWithError('Cannot set-commit on private issue')
1783
1784 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001785 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001786 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001787 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001788 else:
tandrii4b233bd2016-07-06 03:50:29 -07001789 assert new_state == _CQState.DRY_RUN
1790 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001791
1792
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001793 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1794 directory):
1795 # TODO(maruel): Use apply_issue.py
1796
1797 # PatchIssue should never be called with a dirty tree. It is up to the
1798 # caller to check this, but just in case we assert here since the
1799 # consequences of the caller not checking this could be dire.
1800 assert(not git_common.is_dirty_git_tree('apply'))
1801 assert(parsed_issue_arg.valid)
1802 self._changelist.issue = parsed_issue_arg.issue
1803 if parsed_issue_arg.hostname:
1804 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1805
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001806 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1807 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001808 assert parsed_issue_arg.patchset
1809 patchset = parsed_issue_arg.patchset
1810 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1811 else:
1812 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1813 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1814
1815 # Switch up to the top-level directory, if necessary, in preparation for
1816 # applying the patch.
1817 top = settings.GetRelativeRoot()
1818 if top:
1819 os.chdir(top)
1820
1821 # Git patches have a/ at the beginning of source paths. We strip that out
1822 # with a sed script rather than the -p flag to patch so we can feed either
1823 # Git or svn-style patches into the same apply command.
1824 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1825 try:
1826 patch_data = subprocess2.check_output(
1827 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1828 except subprocess2.CalledProcessError:
1829 DieWithError('Git patch mungling failed.')
1830 logging.info(patch_data)
1831
1832 # We use "git apply" to apply the patch instead of "patch" so that we can
1833 # pick up file adds.
1834 # The --index flag means: also insert into the index (so we catch adds).
1835 cmd = ['git', 'apply', '--index', '-p0']
1836 if directory:
1837 cmd.extend(('--directory', directory))
1838 if reject:
1839 cmd.append('--reject')
1840 elif IsGitVersionAtLeast('1.7.12'):
1841 cmd.append('--3way')
1842 try:
1843 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1844 stdin=patch_data, stdout=subprocess2.VOID)
1845 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001846 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001847 return 1
1848
1849 # If we had an issue, commit the current state and register the issue.
1850 if not nocommit:
1851 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1852 'patch from issue %(i)s at patchset '
1853 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1854 % {'i': self.GetIssue(), 'p': patchset})])
1855 self.SetIssue(self.GetIssue())
1856 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001857 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001858 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001859 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001860 return 0
1861
1862 @staticmethod
1863 def ParseIssueURL(parsed_url):
1864 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1865 return None
wychen3c1c1722016-08-04 11:46:36 -07001866 # Rietveld patch: https://domain/<number>/#ps<patchset>
1867 match = re.match(r'/(\d+)/$', parsed_url.path)
1868 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1869 if match and match2:
1870 return _RietveldParsedIssueNumberArgument(
1871 issue=int(match.group(1)),
1872 patchset=int(match2.group(1)),
1873 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001874 # Typical url: https://domain/<issue_number>[/[other]]
1875 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1876 if match:
1877 return _RietveldParsedIssueNumberArgument(
1878 issue=int(match.group(1)),
1879 hostname=parsed_url.netloc)
1880 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1881 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1882 if match:
1883 return _RietveldParsedIssueNumberArgument(
1884 issue=int(match.group(1)),
1885 patchset=int(match.group(2)),
1886 hostname=parsed_url.netloc,
1887 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1888 return None
1889
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001890 def CMDUploadChange(self, options, args, change):
1891 """Upload the patch to Rietveld."""
1892 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1893 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001894 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1895 if options.emulate_svn_auto_props:
1896 upload_args.append('--emulate_svn_auto_props')
1897
1898 change_desc = None
1899
1900 if options.email is not None:
1901 upload_args.extend(['--email', options.email])
1902
1903 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001904 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001905 upload_args.extend(['--title', options.title])
1906 if options.message:
1907 upload_args.extend(['--message', options.message])
1908 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001909 print('This branch is associated with issue %s. '
1910 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001911 else:
nodirca166002016-06-27 10:59:51 -07001912 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001913 upload_args.extend(['--title', options.title])
1914 message = (options.title or options.message or
1915 CreateDescriptionFromLog(args))
1916 change_desc = ChangeDescription(message)
1917 if options.reviewers or options.tbr_owners:
1918 change_desc.update_reviewers(options.reviewers,
1919 options.tbr_owners,
1920 change)
1921 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001922 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001923
1924 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001925 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001926 return 1
1927
1928 upload_args.extend(['--message', change_desc.description])
1929 if change_desc.get_reviewers():
1930 upload_args.append('--reviewers=%s' % ','.join(
1931 change_desc.get_reviewers()))
1932 if options.send_mail:
1933 if not change_desc.get_reviewers():
1934 DieWithError("Must specify reviewers to send email.")
1935 upload_args.append('--send_mail')
1936
1937 # We check this before applying rietveld.private assuming that in
1938 # rietveld.cc only addresses which we can send private CLs to are listed
1939 # if rietveld.private is set, and so we should ignore rietveld.cc only
1940 # when --private is specified explicitly on the command line.
1941 if options.private:
1942 logging.warn('rietveld.cc is ignored since private flag is specified. '
1943 'You need to review and add them manually if necessary.')
1944 cc = self.GetCCListWithoutDefault()
1945 else:
1946 cc = self.GetCCList()
1947 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1948 if cc:
1949 upload_args.extend(['--cc', cc])
1950
1951 if options.private or settings.GetDefaultPrivateFlag() == "True":
1952 upload_args.append('--private')
1953
1954 upload_args.extend(['--git_similarity', str(options.similarity)])
1955 if not options.find_copies:
1956 upload_args.extend(['--git_no_find_copies'])
1957
1958 # Include the upstream repo's URL in the change -- this is useful for
1959 # projects that have their source spread across multiple repos.
1960 remote_url = self.GetGitBaseUrlFromConfig()
1961 if not remote_url:
1962 if settings.GetIsGitSvn():
1963 remote_url = self.GetGitSvnRemoteUrl()
1964 else:
1965 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1966 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1967 self.GetUpstreamBranch().split('/')[-1])
1968 if remote_url:
1969 upload_args.extend(['--base_url', remote_url])
1970 remote, remote_branch = self.GetRemoteBranch()
1971 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1972 settings.GetPendingRefPrefix())
1973 if target_ref:
1974 upload_args.extend(['--target_ref', target_ref])
1975
1976 # Look for dependent patchsets. See crbug.com/480453 for more details.
1977 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1978 upstream_branch = ShortBranchName(upstream_branch)
1979 if remote is '.':
1980 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001981 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001982 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001983 print()
1984 print('Skipping dependency patchset upload because git config '
1985 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1986 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001987 else:
1988 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001989 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001990 auth_config=auth_config)
1991 branch_cl_issue_url = branch_cl.GetIssueURL()
1992 branch_cl_issue = branch_cl.GetIssue()
1993 branch_cl_patchset = branch_cl.GetPatchset()
1994 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1995 upload_args.extend(
1996 ['--depends_on_patchset', '%s:%s' % (
1997 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001998 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001999 '\n'
2000 'The current branch (%s) is tracking a local branch (%s) with '
2001 'an associated CL.\n'
2002 'Adding %s/#ps%s as a dependency patchset.\n'
2003 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2004 branch_cl_patchset))
2005
2006 project = settings.GetProject()
2007 if project:
2008 upload_args.extend(['--project', project])
2009
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002010 try:
2011 upload_args = ['upload'] + upload_args + args
2012 logging.info('upload.RealMain(%s)', upload_args)
2013 issue, patchset = upload.RealMain(upload_args)
2014 issue = int(issue)
2015 patchset = int(patchset)
2016 except KeyboardInterrupt:
2017 sys.exit(1)
2018 except:
2019 # If we got an exception after the user typed a description for their
2020 # change, back up the description before re-raising.
2021 if change_desc:
2022 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2023 print('\nGot exception while uploading -- saving description to %s\n' %
2024 backup_path)
2025 backup_file = open(backup_path, 'w')
2026 backup_file.write(change_desc.description)
2027 backup_file.close()
2028 raise
2029
2030 if not self.GetIssue():
2031 self.SetIssue(issue)
2032 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002033 return 0
2034
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002035
2036class _GerritChangelistImpl(_ChangelistCodereviewBase):
2037 def __init__(self, changelist, auth_config=None):
2038 # auth_config is Rietveld thing, kept here to preserve interface only.
2039 super(_GerritChangelistImpl, self).__init__(changelist)
2040 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002041 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002042 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002043 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002044
2045 def _GetGerritHost(self):
2046 # Lazy load of configs.
2047 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002048 if self._gerrit_host and '.' not in self._gerrit_host:
2049 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2050 # This happens for internal stuff http://crbug.com/614312.
2051 parsed = urlparse.urlparse(self.GetRemoteUrl())
2052 if parsed.scheme == 'sso':
2053 print('WARNING: using non https URLs for remote is likely broken\n'
2054 ' Your current remote is: %s' % self.GetRemoteUrl())
2055 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2056 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002057 return self._gerrit_host
2058
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002059 def _GetGitHost(self):
2060 """Returns git host to be used when uploading change to Gerrit."""
2061 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2062
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002063 def GetCodereviewServer(self):
2064 if not self._gerrit_server:
2065 # If we're on a branch then get the server potentially associated
2066 # with that branch.
2067 if self.GetIssue():
2068 gerrit_server_setting = self.GetCodereviewServerSetting()
2069 if gerrit_server_setting:
2070 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2071 error_ok=True).strip()
2072 if self._gerrit_server:
2073 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2074 if not self._gerrit_server:
2075 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2076 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002077 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002078 parts[0] = parts[0] + '-review'
2079 self._gerrit_host = '.'.join(parts)
2080 self._gerrit_server = 'https://%s' % self._gerrit_host
2081 return self._gerrit_server
2082
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002083 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002084 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002085 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002086
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002087 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002088 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002089 if settings.GetGerritSkipEnsureAuthenticated():
2090 # For projects with unusual authentication schemes.
2091 # See http://crbug.com/603378.
2092 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002093 # Lazy-loader to identify Gerrit and Git hosts.
2094 if gerrit_util.GceAuthenticator.is_gce():
2095 return
2096 self.GetCodereviewServer()
2097 git_host = self._GetGitHost()
2098 assert self._gerrit_server and self._gerrit_host
2099 cookie_auth = gerrit_util.CookiesAuthenticator()
2100
2101 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2102 git_auth = cookie_auth.get_auth_header(git_host)
2103 if gerrit_auth and git_auth:
2104 if gerrit_auth == git_auth:
2105 return
2106 print((
2107 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2108 ' Check your %s or %s file for credentials of hosts:\n'
2109 ' %s\n'
2110 ' %s\n'
2111 ' %s') %
2112 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2113 git_host, self._gerrit_host,
2114 cookie_auth.get_new_password_message(git_host)))
2115 if not force:
2116 ask_for_data('If you know what you are doing, press Enter to continue, '
2117 'Ctrl+C to abort.')
2118 return
2119 else:
2120 missing = (
2121 [] if gerrit_auth else [self._gerrit_host] +
2122 [] if git_auth else [git_host])
2123 DieWithError('Credentials for the following hosts are required:\n'
2124 ' %s\n'
2125 'These are read from %s (or legacy %s)\n'
2126 '%s' % (
2127 '\n '.join(missing),
2128 cookie_auth.get_gitcookies_path(),
2129 cookie_auth.get_netrc_path(),
2130 cookie_auth.get_new_password_message(git_host)))
2131
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002132
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002133 def PatchsetSetting(self):
2134 """Return the git setting that stores this change's most recent patchset."""
2135 return 'branch.%s.gerritpatchset' % self.GetBranch()
2136
2137 def GetCodereviewServerSetting(self):
2138 """Returns the git setting that stores this change's Gerrit server."""
2139 branch = self.GetBranch()
2140 if branch:
2141 return 'branch.%s.gerritserver' % branch
2142 return None
2143
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002144 def _PostUnsetIssueProperties(self):
2145 """Which branch-specific properties to erase when unsetting issue."""
2146 return [
2147 'gerritserver',
2148 'gerritsquashhash',
2149 ]
2150
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002151 def GetRieveldObjForPresubmit(self):
2152 class ThisIsNotRietveldIssue(object):
2153 def __nonzero__(self):
2154 # This is a hack to make presubmit_support think that rietveld is not
2155 # defined, yet still ensure that calls directly result in a decent
2156 # exception message below.
2157 return False
2158
2159 def __getattr__(self, attr):
2160 print(
2161 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2162 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2163 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2164 'or use Rietveld for codereview.\n'
2165 'See also http://crbug.com/579160.' % attr)
2166 raise NotImplementedError()
2167 return ThisIsNotRietveldIssue()
2168
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002169 def GetGerritObjForPresubmit(self):
2170 return presubmit_support.GerritAccessor(self._GetGerritHost())
2171
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002172 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002173 """Apply a rough heuristic to give a simple summary of an issue's review
2174 or CQ status, assuming adherence to a common workflow.
2175
2176 Returns None if no issue for this branch, or one of the following keywords:
2177 * 'error' - error from review tool (including deleted issues)
2178 * 'unsent' - no reviewers added
2179 * 'waiting' - waiting for review
2180 * 'reply' - waiting for owner to reply to review
2181 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2182 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2183 * 'commit' - in the commit queue
2184 * 'closed' - abandoned
2185 """
2186 if not self.GetIssue():
2187 return None
2188
2189 try:
2190 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2191 except httplib.HTTPException:
2192 return 'error'
2193
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002194 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002195 return 'closed'
2196
2197 cq_label = data['labels'].get('Commit-Queue', {})
2198 if cq_label:
2199 # Vote value is a stringified integer, which we expect from 0 to 2.
2200 vote_value = cq_label.get('value', '0')
2201 vote_text = cq_label.get('values', {}).get(vote_value, '')
2202 if vote_text.lower() == 'commit':
2203 return 'commit'
2204
2205 lgtm_label = data['labels'].get('Code-Review', {})
2206 if lgtm_label:
2207 if 'rejected' in lgtm_label:
2208 return 'not lgtm'
2209 if 'approved' in lgtm_label:
2210 return 'lgtm'
2211
2212 if not data.get('reviewers', {}).get('REVIEWER', []):
2213 return 'unsent'
2214
2215 messages = data.get('messages', [])
2216 if messages:
2217 owner = data['owner'].get('_account_id')
2218 last_message_author = messages[-1].get('author', {}).get('_account_id')
2219 if owner != last_message_author:
2220 # Some reply from non-owner.
2221 return 'reply'
2222
2223 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002224
2225 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002226 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002227 return data['revisions'][data['current_revision']]['_number']
2228
2229 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002230 data = self._GetChangeDetail(['CURRENT_REVISION'])
2231 current_rev = data['current_revision']
2232 url = data['revisions'][current_rev]['fetch']['http']['url']
2233 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002234
2235 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002236 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2237 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002238
2239 def CloseIssue(self):
2240 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2241
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002242 def GetApprovingReviewers(self):
2243 """Returns a list of reviewers approving the change.
2244
2245 Note: not necessarily committers.
2246 """
2247 raise NotImplementedError()
2248
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002249 def SubmitIssue(self, wait_for_merge=True):
2250 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2251 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002252
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002253 def _GetChangeDetail(self, options=None, issue=None):
2254 options = options or []
2255 issue = issue or self.GetIssue()
2256 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002257 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2258 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002259
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002260 def CMDLand(self, force, bypass_hooks, verbose):
2261 if git_common.is_dirty_git_tree('land'):
2262 return 1
tandriid60367b2016-06-22 05:25:12 -07002263 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2264 if u'Commit-Queue' in detail.get('labels', {}):
2265 if not force:
2266 ask_for_data('\nIt seems this repository has a Commit Queue, '
2267 'which can test and land changes for you. '
2268 'Are you sure you wish to bypass it?\n'
2269 'Press Enter to continue, Ctrl+C to abort.')
2270
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002271 differs = True
2272 last_upload = RunGit(['config',
2273 'branch.%s.gerritsquashhash' % self.GetBranch()],
2274 error_ok=True).strip()
2275 # Note: git diff outputs nothing if there is no diff.
2276 if not last_upload or RunGit(['diff', last_upload]).strip():
2277 print('WARNING: some changes from local branch haven\'t been uploaded')
2278 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002279 if detail['current_revision'] == last_upload:
2280 differs = False
2281 else:
2282 print('WARNING: local branch contents differ from latest uploaded '
2283 'patchset')
2284 if differs:
2285 if not force:
2286 ask_for_data(
2287 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2288 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2289 elif not bypass_hooks:
2290 hook_results = self.RunHook(
2291 committing=True,
2292 may_prompt=not force,
2293 verbose=verbose,
2294 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2295 if not hook_results.should_continue():
2296 return 1
2297
2298 self.SubmitIssue(wait_for_merge=True)
2299 print('Issue %s has been submitted.' % self.GetIssueURL())
2300 return 0
2301
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002302 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2303 directory):
2304 assert not reject
2305 assert not nocommit
2306 assert not directory
2307 assert parsed_issue_arg.valid
2308
2309 self._changelist.issue = parsed_issue_arg.issue
2310
2311 if parsed_issue_arg.hostname:
2312 self._gerrit_host = parsed_issue_arg.hostname
2313 self._gerrit_server = 'https://%s' % self._gerrit_host
2314
2315 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2316
2317 if not parsed_issue_arg.patchset:
2318 # Use current revision by default.
2319 revision_info = detail['revisions'][detail['current_revision']]
2320 patchset = int(revision_info['_number'])
2321 else:
2322 patchset = parsed_issue_arg.patchset
2323 for revision_info in detail['revisions'].itervalues():
2324 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2325 break
2326 else:
2327 DieWithError('Couldn\'t find patchset %i in issue %i' %
2328 (parsed_issue_arg.patchset, self.GetIssue()))
2329
2330 fetch_info = revision_info['fetch']['http']
2331 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2332 RunGit(['cherry-pick', 'FETCH_HEAD'])
2333 self.SetIssue(self.GetIssue())
2334 self.SetPatchset(patchset)
2335 print('Committed patch for issue %i pathset %i locally' %
2336 (self.GetIssue(), self.GetPatchset()))
2337 return 0
2338
2339 @staticmethod
2340 def ParseIssueURL(parsed_url):
2341 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2342 return None
2343 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2344 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2345 # Short urls like https://domain/<issue_number> can be used, but don't allow
2346 # specifying the patchset (you'd 404), but we allow that here.
2347 if parsed_url.path == '/':
2348 part = parsed_url.fragment
2349 else:
2350 part = parsed_url.path
2351 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2352 if match:
2353 return _ParsedIssueNumberArgument(
2354 issue=int(match.group(2)),
2355 patchset=int(match.group(4)) if match.group(4) else None,
2356 hostname=parsed_url.netloc)
2357 return None
2358
tandrii16e0b4e2016-06-07 10:34:28 -07002359 def _GerritCommitMsgHookCheck(self, offer_removal):
2360 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2361 if not os.path.exists(hook):
2362 return
2363 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2364 # custom developer made one.
2365 data = gclient_utils.FileRead(hook)
2366 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2367 return
2368 print('Warning: you have Gerrit commit-msg hook installed.\n'
2369 'It is not neccessary for uploading with git cl in squash mode, '
2370 'and may interfere with it in subtle ways.\n'
2371 'We recommend you remove the commit-msg hook.')
2372 if offer_removal:
2373 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2374 if reply.lower().startswith('y'):
2375 gclient_utils.rm_file_or_tree(hook)
2376 print('Gerrit commit-msg hook removed.')
2377 else:
2378 print('OK, will keep Gerrit commit-msg hook in place.')
2379
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002380 def CMDUploadChange(self, options, args, change):
2381 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002382 if options.squash and options.no_squash:
2383 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002384
2385 if not options.squash and not options.no_squash:
2386 # Load default for user, repo, squash=true, in this order.
2387 options.squash = settings.GetSquashGerritUploads()
2388 elif options.no_squash:
2389 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002390
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002391 # We assume the remote called "origin" is the one we want.
2392 # It is probably not worthwhile to support different workflows.
2393 gerrit_remote = 'origin'
2394
2395 remote, remote_branch = self.GetRemoteBranch()
2396 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2397 pending_prefix='')
2398
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002399 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002400 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002401 if self.GetIssue():
2402 # Try to get the message from a previous upload.
2403 message = self.GetDescription()
2404 if not message:
2405 DieWithError(
2406 'failed to fetch description from current Gerrit issue %d\n'
2407 '%s' % (self.GetIssue(), self.GetIssueURL()))
2408 change_id = self._GetChangeDetail()['change_id']
2409 while True:
2410 footer_change_ids = git_footers.get_footer_change_id(message)
2411 if footer_change_ids == [change_id]:
2412 break
2413 if not footer_change_ids:
2414 message = git_footers.add_footer_change_id(message, change_id)
2415 print('WARNING: appended missing Change-Id to issue description')
2416 continue
2417 # There is already a valid footer but with different or several ids.
2418 # Doing this automatically is non-trivial as we don't want to lose
2419 # existing other footers, yet we want to append just 1 desired
2420 # Change-Id. Thus, just create a new footer, but let user verify the
2421 # new description.
2422 message = '%s\n\nChange-Id: %s' % (message, change_id)
2423 print(
2424 'WARNING: issue %s has Change-Id footer(s):\n'
2425 ' %s\n'
2426 'but issue has Change-Id %s, according to Gerrit.\n'
2427 'Please, check the proposed correction to the description, '
2428 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2429 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2430 change_id))
2431 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2432 if not options.force:
2433 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002434 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002435 message = change_desc.description
2436 if not message:
2437 DieWithError("Description is empty. Aborting...")
2438 # Continue the while loop.
2439 # Sanity check of this code - we should end up with proper message
2440 # footer.
2441 assert [change_id] == git_footers.get_footer_change_id(message)
2442 change_desc = ChangeDescription(message)
2443 else:
2444 change_desc = ChangeDescription(
2445 options.message or CreateDescriptionFromLog(args))
2446 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002447 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002448 if not change_desc.description:
2449 DieWithError("Description is empty. Aborting...")
2450 message = change_desc.description
2451 change_ids = git_footers.get_footer_change_id(message)
2452 if len(change_ids) > 1:
2453 DieWithError('too many Change-Id footers, at most 1 allowed.')
2454 if not change_ids:
2455 # Generate the Change-Id automatically.
2456 message = git_footers.add_footer_change_id(
2457 message, GenerateGerritChangeId(message))
2458 change_desc.set_description(message)
2459 change_ids = git_footers.get_footer_change_id(message)
2460 assert len(change_ids) == 1
2461 change_id = change_ids[0]
2462
2463 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2464 if remote is '.':
2465 # If our upstream branch is local, we base our squashed commit on its
2466 # squashed version.
2467 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2468 # Check the squashed hash of the parent.
2469 parent = RunGit(['config',
2470 'branch.%s.gerritsquashhash' % upstream_branch_name],
2471 error_ok=True).strip()
2472 # Verify that the upstream branch has been uploaded too, otherwise
2473 # Gerrit will create additional CLs when uploading.
2474 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2475 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002476 DieWithError(
2477 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002478 'Note: maybe you\'ve uploaded it with --no-squash. '
2479 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002480 ' git cl upload --squash\n' % upstream_branch_name)
2481 else:
2482 parent = self.GetCommonAncestorWithUpstream()
2483
2484 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2485 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2486 '-m', message]).strip()
2487 else:
2488 change_desc = ChangeDescription(
2489 options.message or CreateDescriptionFromLog(args))
2490 if not change_desc.description:
2491 DieWithError("Description is empty. Aborting...")
2492
2493 if not git_footers.get_footer_change_id(change_desc.description):
2494 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002495 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2496 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002497 ref_to_push = 'HEAD'
2498 parent = '%s/%s' % (gerrit_remote, branch)
2499 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2500
2501 assert change_desc
2502 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2503 ref_to_push)]).splitlines()
2504 if len(commits) > 1:
2505 print('WARNING: This will upload %d commits. Run the following command '
2506 'to see which commits will be uploaded: ' % len(commits))
2507 print('git log %s..%s' % (parent, ref_to_push))
2508 print('You can also use `git squash-branch` to squash these into a '
2509 'single commit.')
2510 ask_for_data('About to upload; enter to confirm.')
2511
2512 if options.reviewers or options.tbr_owners:
2513 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2514 change)
2515
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002516 # Extra options that can be specified at push time. Doc:
2517 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2518 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002519 if change_desc.get_reviewers(tbr_only=True):
2520 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2521 refspec_opts.append('l=Code-Review+1')
2522
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002523 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002524 if not re.match(r'^[\w ]+$', options.title):
2525 options.title = re.sub(r'[^\w ]', '', options.title)
2526 print('WARNING: Patchset title may only contain alphanumeric chars '
2527 'and spaces. Cleaned up title:\n%s' % options.title)
2528 if not options.force:
2529 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002530 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2531 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002532 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2533
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002534 if options.send_mail:
2535 if not change_desc.get_reviewers():
2536 DieWithError('Must specify reviewers to send email.')
2537 refspec_opts.append('notify=ALL')
2538 else:
2539 refspec_opts.append('notify=NONE')
2540
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002541 cc = self.GetCCList().split(',')
2542 if options.cc:
2543 cc.extend(options.cc)
2544 cc = filter(None, cc)
2545 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002546 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002547
tandrii99a72f22016-08-17 14:33:24 -07002548 reviewers = change_desc.get_reviewers()
2549 if reviewers:
2550 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002551
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002552 refspec_suffix = ''
2553 if refspec_opts:
2554 refspec_suffix = '%' + ','.join(refspec_opts)
2555 assert ' ' not in refspec_suffix, (
2556 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002557 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002558
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002559 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002560 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002561 print_stdout=True,
2562 # Flush after every line: useful for seeing progress when running as
2563 # recipe.
2564 filter_fn=lambda _: sys.stdout.flush())
2565
2566 if options.squash:
2567 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2568 change_numbers = [m.group(1)
2569 for m in map(regex.match, push_stdout.splitlines())
2570 if m]
2571 if len(change_numbers) != 1:
2572 DieWithError(
2573 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2574 'Change-Id: %s') % (len(change_numbers), change_id))
2575 self.SetIssue(change_numbers[0])
2576 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2577 ref_to_push])
2578 return 0
2579
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002580 def _AddChangeIdToCommitMessage(self, options, args):
2581 """Re-commits using the current message, assumes the commit hook is in
2582 place.
2583 """
2584 log_desc = options.message or CreateDescriptionFromLog(args)
2585 git_command = ['commit', '--amend', '-m', log_desc]
2586 RunGit(git_command)
2587 new_log_desc = CreateDescriptionFromLog(args)
2588 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002589 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002590 return new_log_desc
2591 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002592 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002593
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002594 def SetCQState(self, new_state):
2595 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002596 vote_map = {
2597 _CQState.NONE: 0,
2598 _CQState.DRY_RUN: 1,
2599 _CQState.COMMIT : 2,
2600 }
2601 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2602 labels={'Commit-Queue': vote_map[new_state]})
2603
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002604
2605_CODEREVIEW_IMPLEMENTATIONS = {
2606 'rietveld': _RietveldChangelistImpl,
2607 'gerrit': _GerritChangelistImpl,
2608}
2609
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002610
iannuccie53c9352016-08-17 14:40:40 -07002611def _add_codereview_issue_select_options(parser, extra=""):
2612 _add_codereview_select_options(parser)
2613
2614 text = ('Operate on this issue number instead of the current branch\'s '
2615 'implicit issue.')
2616 if extra:
2617 text += ' '+extra
2618 parser.add_option('-i', '--issue', type=int, help=text)
2619
2620
2621def _process_codereview_issue_select_options(parser, options):
2622 _process_codereview_select_options(parser, options)
2623 if options.issue is not None and not options.forced_codereview:
2624 parser.error('--issue must be specified with either --rietveld or --gerrit')
2625
2626
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002627def _add_codereview_select_options(parser):
2628 """Appends --gerrit and --rietveld options to force specific codereview."""
2629 parser.codereview_group = optparse.OptionGroup(
2630 parser, 'EXPERIMENTAL! Codereview override options')
2631 parser.add_option_group(parser.codereview_group)
2632 parser.codereview_group.add_option(
2633 '--gerrit', action='store_true',
2634 help='Force the use of Gerrit for codereview')
2635 parser.codereview_group.add_option(
2636 '--rietveld', action='store_true',
2637 help='Force the use of Rietveld for codereview')
2638
2639
2640def _process_codereview_select_options(parser, options):
2641 if options.gerrit and options.rietveld:
2642 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2643 options.forced_codereview = None
2644 if options.gerrit:
2645 options.forced_codereview = 'gerrit'
2646 elif options.rietveld:
2647 options.forced_codereview = 'rietveld'
2648
2649
tandriif9aefb72016-07-01 09:06:51 -07002650def _get_bug_line_values(default_project, bugs):
2651 """Given default_project and comma separated list of bugs, yields bug line
2652 values.
2653
2654 Each bug can be either:
2655 * a number, which is combined with default_project
2656 * string, which is left as is.
2657
2658 This function may produce more than one line, because bugdroid expects one
2659 project per line.
2660
2661 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2662 ['v8:123', 'chromium:789']
2663 """
2664 default_bugs = []
2665 others = []
2666 for bug in bugs.split(','):
2667 bug = bug.strip()
2668 if bug:
2669 try:
2670 default_bugs.append(int(bug))
2671 except ValueError:
2672 others.append(bug)
2673
2674 if default_bugs:
2675 default_bugs = ','.join(map(str, default_bugs))
2676 if default_project:
2677 yield '%s:%s' % (default_project, default_bugs)
2678 else:
2679 yield default_bugs
2680 for other in sorted(others):
2681 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2682 yield other
2683
2684
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002685class ChangeDescription(object):
2686 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002687 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002688 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002689
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002690 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002691 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002692
agable@chromium.org42c20792013-09-12 17:34:49 +00002693 @property # www.logilab.org/ticket/89786
2694 def description(self): # pylint: disable=E0202
2695 return '\n'.join(self._description_lines)
2696
2697 def set_description(self, desc):
2698 if isinstance(desc, basestring):
2699 lines = desc.splitlines()
2700 else:
2701 lines = [line.rstrip() for line in desc]
2702 while lines and not lines[0]:
2703 lines.pop(0)
2704 while lines and not lines[-1]:
2705 lines.pop(-1)
2706 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002707
piman@chromium.org336f9122014-09-04 02:16:55 +00002708 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002709 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002710 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002711 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002712 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002713 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002714
agable@chromium.org42c20792013-09-12 17:34:49 +00002715 # Get the set of R= and TBR= lines and remove them from the desciption.
2716 regexp = re.compile(self.R_LINE)
2717 matches = [regexp.match(line) for line in self._description_lines]
2718 new_desc = [l for i, l in enumerate(self._description_lines)
2719 if not matches[i]]
2720 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002721
agable@chromium.org42c20792013-09-12 17:34:49 +00002722 # Construct new unified R= and TBR= lines.
2723 r_names = []
2724 tbr_names = []
2725 for match in matches:
2726 if not match:
2727 continue
2728 people = cleanup_list([match.group(2).strip()])
2729 if match.group(1) == 'TBR':
2730 tbr_names.extend(people)
2731 else:
2732 r_names.extend(people)
2733 for name in r_names:
2734 if name not in reviewers:
2735 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002736 if add_owners_tbr:
2737 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002738 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002739 all_reviewers = set(tbr_names + reviewers)
2740 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2741 all_reviewers)
2742 tbr_names.extend(owners_db.reviewers_for(missing_files,
2743 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002744 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2745 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2746
2747 # Put the new lines in the description where the old first R= line was.
2748 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2749 if 0 <= line_loc < len(self._description_lines):
2750 if new_tbr_line:
2751 self._description_lines.insert(line_loc, new_tbr_line)
2752 if new_r_line:
2753 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002754 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002755 if new_r_line:
2756 self.append_footer(new_r_line)
2757 if new_tbr_line:
2758 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002759
tandriif9aefb72016-07-01 09:06:51 -07002760 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002761 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002762 self.set_description([
2763 '# Enter a description of the change.',
2764 '# This will be displayed on the codereview site.',
2765 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002766 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002767 '--------------------',
2768 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002769
agable@chromium.org42c20792013-09-12 17:34:49 +00002770 regexp = re.compile(self.BUG_LINE)
2771 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002772 prefix = settings.GetBugPrefix()
2773 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2774 for value in values:
2775 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2776 self.append_footer('BUG=%s' % value)
2777
agable@chromium.org42c20792013-09-12 17:34:49 +00002778 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002779 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002780 if not content:
2781 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002782 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002783
2784 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002785 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2786 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002787 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002788 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002789
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002790 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002791 """Adds a footer line to the description.
2792
2793 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2794 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2795 that Gerrit footers are always at the end.
2796 """
2797 parsed_footer_line = git_footers.parse_footer(line)
2798 if parsed_footer_line:
2799 # Line is a gerrit footer in the form: Footer-Key: any value.
2800 # Thus, must be appended observing Gerrit footer rules.
2801 self.set_description(
2802 git_footers.add_footer(self.description,
2803 key=parsed_footer_line[0],
2804 value=parsed_footer_line[1]))
2805 return
2806
2807 if not self._description_lines:
2808 self._description_lines.append(line)
2809 return
2810
2811 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2812 if gerrit_footers:
2813 # git_footers.split_footers ensures that there is an empty line before
2814 # actual (gerrit) footers, if any. We have to keep it that way.
2815 assert top_lines and top_lines[-1] == ''
2816 top_lines, separator = top_lines[:-1], top_lines[-1:]
2817 else:
2818 separator = [] # No need for separator if there are no gerrit_footers.
2819
2820 prev_line = top_lines[-1] if top_lines else ''
2821 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2822 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2823 top_lines.append('')
2824 top_lines.append(line)
2825 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002826
tandrii99a72f22016-08-17 14:33:24 -07002827 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002828 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002829 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002830 reviewers = [match.group(2).strip()
2831 for match in matches
2832 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002833 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002834
2835
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002836def get_approving_reviewers(props):
2837 """Retrieves the reviewers that approved a CL from the issue properties with
2838 messages.
2839
2840 Note that the list may contain reviewers that are not committer, thus are not
2841 considered by the CQ.
2842 """
2843 return sorted(
2844 set(
2845 message['sender']
2846 for message in props['messages']
2847 if message['approval'] and message['sender'] in props['reviewers']
2848 )
2849 )
2850
2851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852def FindCodereviewSettingsFile(filename='codereview.settings'):
2853 """Finds the given file starting in the cwd and going up.
2854
2855 Only looks up to the top of the repository unless an
2856 'inherit-review-settings-ok' file exists in the root of the repository.
2857 """
2858 inherit_ok_file = 'inherit-review-settings-ok'
2859 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002860 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002861 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2862 root = '/'
2863 while True:
2864 if filename in os.listdir(cwd):
2865 if os.path.isfile(os.path.join(cwd, filename)):
2866 return open(os.path.join(cwd, filename))
2867 if cwd == root:
2868 break
2869 cwd = os.path.dirname(cwd)
2870
2871
2872def LoadCodereviewSettingsFromFile(fileobj):
2873 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002874 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002875
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002876 def SetProperty(name, setting, unset_error_ok=False):
2877 fullname = 'rietveld.' + name
2878 if setting in keyvals:
2879 RunGit(['config', fullname, keyvals[setting]])
2880 else:
2881 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2882
2883 SetProperty('server', 'CODE_REVIEW_SERVER')
2884 # Only server setting is required. Other settings can be absent.
2885 # In that case, we ignore errors raised during option deletion attempt.
2886 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002887 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002888 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2889 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002890 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002891 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002892 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2893 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002894 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002895 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002896 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002897 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2898 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002899
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002900 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002901 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002902
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002903 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002904 RunGit(['config', 'gerrit.squash-uploads',
2905 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002906
tandrii@chromium.org28253532016-04-14 13:46:56 +00002907 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002908 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002909 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2912 #should be of the form
2913 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2914 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2915 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2916 keyvals['ORIGIN_URL_CONFIG']])
2917
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002918
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002919def urlretrieve(source, destination):
2920 """urllib is broken for SSL connections via a proxy therefore we
2921 can't use urllib.urlretrieve()."""
2922 with open(destination, 'w') as f:
2923 f.write(urllib2.urlopen(source).read())
2924
2925
ukai@chromium.org712d6102013-11-27 00:52:58 +00002926def hasSheBang(fname):
2927 """Checks fname is a #! script."""
2928 with open(fname) as f:
2929 return f.read(2).startswith('#!')
2930
2931
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002932# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2933def DownloadHooks(*args, **kwargs):
2934 pass
2935
2936
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002937def DownloadGerritHook(force):
2938 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002939
2940 Args:
2941 force: True to update hooks. False to install hooks if not present.
2942 """
2943 if not settings.GetIsGerrit():
2944 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002945 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002946 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2947 if not os.access(dst, os.X_OK):
2948 if os.path.exists(dst):
2949 if not force:
2950 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002951 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002952 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002953 if not hasSheBang(dst):
2954 DieWithError('Not a script: %s\n'
2955 'You need to download from\n%s\n'
2956 'into .git/hooks/commit-msg and '
2957 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002958 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2959 except Exception:
2960 if os.path.exists(dst):
2961 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002962 DieWithError('\nFailed to download hooks.\n'
2963 'You need to download from\n%s\n'
2964 'into .git/hooks/commit-msg and '
2965 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002966
2967
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002968
2969def GetRietveldCodereviewSettingsInteractively():
2970 """Prompt the user for settings."""
2971 server = settings.GetDefaultServerUrl(error_ok=True)
2972 prompt = 'Rietveld server (host[:port])'
2973 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2974 newserver = ask_for_data(prompt + ':')
2975 if not server and not newserver:
2976 newserver = DEFAULT_SERVER
2977 if newserver:
2978 newserver = gclient_utils.UpgradeToHttps(newserver)
2979 if newserver != server:
2980 RunGit(['config', 'rietveld.server', newserver])
2981
2982 def SetProperty(initial, caption, name, is_url):
2983 prompt = caption
2984 if initial:
2985 prompt += ' ("x" to clear) [%s]' % initial
2986 new_val = ask_for_data(prompt + ':')
2987 if new_val == 'x':
2988 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2989 elif new_val:
2990 if is_url:
2991 new_val = gclient_utils.UpgradeToHttps(new_val)
2992 if new_val != initial:
2993 RunGit(['config', 'rietveld.' + name, new_val])
2994
2995 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2996 SetProperty(settings.GetDefaultPrivateFlag(),
2997 'Private flag (rietveld only)', 'private', False)
2998 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2999 'tree-status-url', False)
3000 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3001 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3002 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3003 'run-post-upload-hook', False)
3004
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003005@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003006def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003007 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003008
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003009 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003010 'For Gerrit, see http://crbug.com/603116.')
3011 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003012 parser.add_option('--activate-update', action='store_true',
3013 help='activate auto-updating [rietveld] section in '
3014 '.git/config')
3015 parser.add_option('--deactivate-update', action='store_true',
3016 help='deactivate auto-updating [rietveld] section in '
3017 '.git/config')
3018 options, args = parser.parse_args(args)
3019
3020 if options.deactivate_update:
3021 RunGit(['config', 'rietveld.autoupdate', 'false'])
3022 return
3023
3024 if options.activate_update:
3025 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3026 return
3027
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003028 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003029 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003030 return 0
3031
3032 url = args[0]
3033 if not url.endswith('codereview.settings'):
3034 url = os.path.join(url, 'codereview.settings')
3035
3036 # Load code review settings and download hooks (if available).
3037 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3038 return 0
3039
3040
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003041def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003042 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003043 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3044 branch = ShortBranchName(branchref)
3045 _, args = parser.parse_args(args)
3046 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003047 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003048 return RunGit(['config', 'branch.%s.base-url' % branch],
3049 error_ok=False).strip()
3050 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003051 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003052 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3053 error_ok=False).strip()
3054
3055
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003056def color_for_status(status):
3057 """Maps a Changelist status to color, for CMDstatus and other tools."""
3058 return {
3059 'unsent': Fore.RED,
3060 'waiting': Fore.BLUE,
3061 'reply': Fore.YELLOW,
3062 'lgtm': Fore.GREEN,
3063 'commit': Fore.MAGENTA,
3064 'closed': Fore.CYAN,
3065 'error': Fore.WHITE,
3066 }.get(status, Fore.WHITE)
3067
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003068
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003069def get_cl_statuses(changes, fine_grained, max_processes=None):
3070 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003071
3072 If fine_grained is true, this will fetch CL statuses from the server.
3073 Otherwise, simply indicate if there's a matching url for the given branches.
3074
3075 If max_processes is specified, it is used as the maximum number of processes
3076 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3077 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003078
3079 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003080 """
3081 # Silence upload.py otherwise it becomes unwieldly.
3082 upload.verbosity = 0
3083
3084 if fine_grained:
3085 # Process one branch synchronously to work through authentication, then
3086 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003087 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003088 def fetch(cl):
3089 try:
3090 return (cl, cl.GetStatus())
3091 except:
3092 # See http://crbug.com/629863.
3093 logging.exception('failed to fetch status for %s:', cl)
3094 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003095 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003096
tandriiea9514a2016-08-17 12:32:37 -07003097 changes_to_fetch = changes[1:]
3098 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003099 # Exit early if there was only one branch to fetch.
3100 return
3101
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003102 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003103 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003104 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003105 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003106
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003107 fetched_cls = set()
3108 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003109 while True:
3110 try:
3111 row = it.next(timeout=5)
3112 except multiprocessing.TimeoutError:
3113 break
3114
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003115 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003116 yield row
3117
3118 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003119 for cl in set(changes_to_fetch) - fetched_cls:
3120 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003121
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003122 else:
3123 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003124 for cl in changes:
3125 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003126
rmistry@google.com2dd99862015-06-22 12:22:18 +00003127
3128def upload_branch_deps(cl, args):
3129 """Uploads CLs of local branches that are dependents of the current branch.
3130
3131 If the local branch dependency tree looks like:
3132 test1 -> test2.1 -> test3.1
3133 -> test3.2
3134 -> test2.2 -> test3.3
3135
3136 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3137 run on the dependent branches in this order:
3138 test2.1, test3.1, test3.2, test2.2, test3.3
3139
3140 Note: This function does not rebase your local dependent branches. Use it when
3141 you make a change to the parent branch that will not conflict with its
3142 dependent branches, and you would like their dependencies updated in
3143 Rietveld.
3144 """
3145 if git_common.is_dirty_git_tree('upload-branch-deps'):
3146 return 1
3147
3148 root_branch = cl.GetBranch()
3149 if root_branch is None:
3150 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3151 'Get on a branch!')
3152 if not cl.GetIssue() or not cl.GetPatchset():
3153 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3154 'patchset dependencies without an uploaded CL.')
3155
3156 branches = RunGit(['for-each-ref',
3157 '--format=%(refname:short) %(upstream:short)',
3158 'refs/heads'])
3159 if not branches:
3160 print('No local branches found.')
3161 return 0
3162
3163 # Create a dictionary of all local branches to the branches that are dependent
3164 # on it.
3165 tracked_to_dependents = collections.defaultdict(list)
3166 for b in branches.splitlines():
3167 tokens = b.split()
3168 if len(tokens) == 2:
3169 branch_name, tracked = tokens
3170 tracked_to_dependents[tracked].append(branch_name)
3171
vapiera7fbd5a2016-06-16 09:17:49 -07003172 print()
3173 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003174 dependents = []
3175 def traverse_dependents_preorder(branch, padding=''):
3176 dependents_to_process = tracked_to_dependents.get(branch, [])
3177 padding += ' '
3178 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003179 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003180 dependents.append(dependent)
3181 traverse_dependents_preorder(dependent, padding)
3182 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003183 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003184
3185 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003186 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003187 return 0
3188
vapiera7fbd5a2016-06-16 09:17:49 -07003189 print('This command will checkout all dependent branches and run '
3190 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003191 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3192
andybons@chromium.org962f9462016-02-03 20:00:42 +00003193 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003194 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003195 args.extend(['-t', 'Updated patchset dependency'])
3196
rmistry@google.com2dd99862015-06-22 12:22:18 +00003197 # Record all dependents that failed to upload.
3198 failures = {}
3199 # Go through all dependents, checkout the branch and upload.
3200 try:
3201 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003202 print()
3203 print('--------------------------------------')
3204 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003205 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003206 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003207 try:
3208 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003209 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003210 failures[dependent_branch] = 1
3211 except: # pylint: disable=W0702
3212 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003213 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003214 finally:
3215 # Swap back to the original root branch.
3216 RunGit(['checkout', '-q', root_branch])
3217
vapiera7fbd5a2016-06-16 09:17:49 -07003218 print()
3219 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003220 for dependent_branch in dependents:
3221 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003222 print(' %s : %s' % (dependent_branch, upload_status))
3223 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003224
3225 return 0
3226
3227
kmarshall3bff56b2016-06-06 18:31:47 -07003228def CMDarchive(parser, args):
3229 """Archives and deletes branches associated with closed changelists."""
3230 parser.add_option(
3231 '-j', '--maxjobs', action='store', type=int,
3232 help='The maximum number of jobs to use when retrieving review status')
3233 parser.add_option(
3234 '-f', '--force', action='store_true',
3235 help='Bypasses the confirmation prompt.')
3236
3237 auth.add_auth_options(parser)
3238 options, args = parser.parse_args(args)
3239 if args:
3240 parser.error('Unsupported args: %s' % ' '.join(args))
3241 auth_config = auth.extract_auth_config_from_options(options)
3242
3243 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3244 if not branches:
3245 return 0
3246
vapiera7fbd5a2016-06-16 09:17:49 -07003247 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003248 changes = [Changelist(branchref=b, auth_config=auth_config)
3249 for b in branches.splitlines()]
3250 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3251 statuses = get_cl_statuses(changes,
3252 fine_grained=True,
3253 max_processes=options.maxjobs)
3254 proposal = [(cl.GetBranch(),
3255 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3256 for cl, status in statuses
3257 if status == 'closed']
3258 proposal.sort()
3259
3260 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003261 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003262 return 0
3263
3264 current_branch = GetCurrentBranch()
3265
vapiera7fbd5a2016-06-16 09:17:49 -07003266 print('\nBranches with closed issues that will be archived:\n')
3267 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003268 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003269 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003270
3271 if any(branch == current_branch for branch, _ in proposal):
3272 print('You are currently on a branch \'%s\' which is associated with a '
3273 'closed codereview issue, so archive cannot proceed. Please '
3274 'checkout another branch and run this command again.' %
3275 current_branch)
3276 return 1
3277
3278 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003279 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3280 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003281 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003282 return 1
3283
3284 for branch, tagname in proposal:
3285 RunGit(['tag', tagname, branch])
3286 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003287 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003288
3289 return 0
3290
3291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003293 """Show status of changelists.
3294
3295 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003296 - Red not sent for review or broken
3297 - Blue waiting for review
3298 - Yellow waiting for you to reply to review
3299 - Green LGTM'ed
3300 - Magenta in the commit queue
3301 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003302
3303 Also see 'git cl comments'.
3304 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003305 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003306 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003307 parser.add_option('-f', '--fast', action='store_true',
3308 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003309 parser.add_option(
3310 '-j', '--maxjobs', action='store', type=int,
3311 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003312
3313 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003314 _add_codereview_issue_select_options(
3315 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003316 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003317 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003318 if args:
3319 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003320 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003321
iannuccie53c9352016-08-17 14:40:40 -07003322 if options.issue is not None and not options.field:
3323 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003324
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003325 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003326 cl = Changelist(auth_config=auth_config, issue=options.issue,
3327 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003328 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003329 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330 elif options.field == 'id':
3331 issueid = cl.GetIssue()
3332 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003333 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003334 elif options.field == 'patch':
3335 patchset = cl.GetPatchset()
3336 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003337 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003338 elif options.field == 'status':
3339 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003340 elif options.field == 'url':
3341 url = cl.GetIssueURL()
3342 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003343 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003344 return 0
3345
3346 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3347 if not branches:
3348 print('No local branch found.')
3349 return 0
3350
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003351 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003352 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003353 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003354 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003355 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003356 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003357 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003358
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003359 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003360 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3361 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3362 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003363 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003364 c, status = output.next()
3365 branch_statuses[c.GetBranch()] = status
3366 status = branch_statuses.pop(branch)
3367 url = cl.GetIssueURL()
3368 if url and (not status or status == 'error'):
3369 # The issue probably doesn't exist anymore.
3370 url += ' (broken)'
3371
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003372 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003373 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003374 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003375 color = ''
3376 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003377 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003378 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003379 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003380 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003381
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003382 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003383 print()
3384 print('Current branch:',)
3385 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003386 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003387 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003388 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003389 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003390 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003391 print('Issue description:')
3392 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003393 return 0
3394
3395
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003396def colorize_CMDstatus_doc():
3397 """To be called once in main() to add colors to git cl status help."""
3398 colors = [i for i in dir(Fore) if i[0].isupper()]
3399
3400 def colorize_line(line):
3401 for color in colors:
3402 if color in line.upper():
3403 # Extract whitespaces first and the leading '-'.
3404 indent = len(line) - len(line.lstrip(' ')) + 1
3405 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3406 return line
3407
3408 lines = CMDstatus.__doc__.splitlines()
3409 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3410
3411
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003412@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003413def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003414 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415
3416 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003417 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003418 parser.add_option('-r', '--reverse', action='store_true',
3419 help='Lookup the branch(es) for the specified issues. If '
3420 'no issues are specified, all branches with mapped '
3421 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003422 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003423 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003424 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003425
dnj@chromium.org406c4402015-03-03 17:22:28 +00003426 if options.reverse:
3427 branches = RunGit(['for-each-ref', 'refs/heads',
3428 '--format=%(refname:short)']).splitlines()
3429
3430 # Reverse issue lookup.
3431 issue_branch_map = {}
3432 for branch in branches:
3433 cl = Changelist(branchref=branch)
3434 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3435 if not args:
3436 args = sorted(issue_branch_map.iterkeys())
3437 for issue in args:
3438 if not issue:
3439 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003440 print('Branch for issue number %s: %s' % (
3441 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003442 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003443 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003444 if len(args) > 0:
3445 try:
3446 issue = int(args[0])
3447 except ValueError:
3448 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003449 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003450 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003451 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003452 return 0
3453
3454
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003455def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003456 """Shows or posts review comments for any changelist."""
3457 parser.add_option('-a', '--add-comment', dest='comment',
3458 help='comment to add to an issue')
3459 parser.add_option('-i', dest='issue',
3460 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003461 parser.add_option('-j', '--json-file',
3462 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003463 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003464 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003465 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003466
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003467 issue = None
3468 if options.issue:
3469 try:
3470 issue = int(options.issue)
3471 except ValueError:
3472 DieWithError('A review issue id is expected to be a number')
3473
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003474 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003475
3476 if options.comment:
3477 cl.AddComment(options.comment)
3478 return 0
3479
3480 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003481 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003482 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003483 summary.append({
3484 'date': message['date'],
3485 'lgtm': False,
3486 'message': message['text'],
3487 'not_lgtm': False,
3488 'sender': message['sender'],
3489 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003490 if message['disapproval']:
3491 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003492 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003493 elif message['approval']:
3494 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003495 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003496 elif message['sender'] == data['owner_email']:
3497 color = Fore.MAGENTA
3498 else:
3499 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003500 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003501 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003502 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003503 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003504 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003505 if options.json_file:
3506 with open(options.json_file, 'wb') as f:
3507 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003508 return 0
3509
3510
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003511@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003512def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003513 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003514 parser.add_option('-d', '--display', action='store_true',
3515 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003516 parser.add_option('-n', '--new-description',
3517 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003518
3519 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003520 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003521 options, args = parser.parse_args(args)
3522 _process_codereview_select_options(parser, options)
3523
3524 target_issue = None
3525 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003526 target_issue = ParseIssueNumberArgument(args[0])
3527 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003528 parser.print_help()
3529 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003530
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003531 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003532
martiniss6eda05f2016-06-30 10:18:35 -07003533 kwargs = {
3534 'auth_config': auth_config,
3535 'codereview': options.forced_codereview,
3536 }
3537 if target_issue:
3538 kwargs['issue'] = target_issue.issue
3539 if options.forced_codereview == 'rietveld':
3540 kwargs['rietveld_server'] = target_issue.hostname
3541
3542 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003543
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003544 if not cl.GetIssue():
3545 DieWithError('This branch has no associated changelist.')
3546 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003547
smut@google.com34fb6b12015-07-13 20:03:26 +00003548 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003549 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003550 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003551
3552 if options.new_description:
3553 text = options.new_description
3554 if text == '-':
3555 text = '\n'.join(l.rstrip() for l in sys.stdin)
3556
3557 description.set_description(text)
3558 else:
3559 description.prompt()
3560
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003561 if cl.GetDescription() != description.description:
3562 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003563 return 0
3564
3565
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003566def CreateDescriptionFromLog(args):
3567 """Pulls out the commit log to use as a base for the CL description."""
3568 log_args = []
3569 if len(args) == 1 and not args[0].endswith('.'):
3570 log_args = [args[0] + '..']
3571 elif len(args) == 1 and args[0].endswith('...'):
3572 log_args = [args[0][:-1]]
3573 elif len(args) == 2:
3574 log_args = [args[0] + '..' + args[1]]
3575 else:
3576 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003577 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003578
3579
thestig@chromium.org44202a22014-03-11 19:22:18 +00003580def CMDlint(parser, args):
3581 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003582 parser.add_option('--filter', action='append', metavar='-x,+y',
3583 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003584 auth.add_auth_options(parser)
3585 options, args = parser.parse_args(args)
3586 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003587
3588 # Access to a protected member _XX of a client class
3589 # pylint: disable=W0212
3590 try:
3591 import cpplint
3592 import cpplint_chromium
3593 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003594 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003595 return 1
3596
3597 # Change the current working directory before calling lint so that it
3598 # shows the correct base.
3599 previous_cwd = os.getcwd()
3600 os.chdir(settings.GetRoot())
3601 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003602 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003603 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3604 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003605 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003606 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003607 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003608
3609 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003610 command = args + files
3611 if options.filter:
3612 command = ['--filter=' + ','.join(options.filter)] + command
3613 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003614
3615 white_regex = re.compile(settings.GetLintRegex())
3616 black_regex = re.compile(settings.GetLintIgnoreRegex())
3617 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3618 for filename in filenames:
3619 if white_regex.match(filename):
3620 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003621 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003622 else:
3623 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3624 extra_check_functions)
3625 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003626 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003627 finally:
3628 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003629 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003630 if cpplint._cpplint_state.error_count != 0:
3631 return 1
3632 return 0
3633
3634
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003635def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003636 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003637 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003638 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003639 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003640 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003641 auth.add_auth_options(parser)
3642 options, args = parser.parse_args(args)
3643 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003644
sbc@chromium.org71437c02015-04-09 19:29:40 +00003645 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003646 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647 return 1
3648
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003649 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003650 if args:
3651 base_branch = args[0]
3652 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003653 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003654 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003655
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003656 cl.RunHook(
3657 committing=not options.upload,
3658 may_prompt=False,
3659 verbose=options.verbose,
3660 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003661 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662
3663
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003664def GenerateGerritChangeId(message):
3665 """Returns Ixxxxxx...xxx change id.
3666
3667 Works the same way as
3668 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3669 but can be called on demand on all platforms.
3670
3671 The basic idea is to generate git hash of a state of the tree, original commit
3672 message, author/committer info and timestamps.
3673 """
3674 lines = []
3675 tree_hash = RunGitSilent(['write-tree'])
3676 lines.append('tree %s' % tree_hash.strip())
3677 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3678 if code == 0:
3679 lines.append('parent %s' % parent.strip())
3680 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3681 lines.append('author %s' % author.strip())
3682 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3683 lines.append('committer %s' % committer.strip())
3684 lines.append('')
3685 # Note: Gerrit's commit-hook actually cleans message of some lines and
3686 # whitespace. This code is not doing this, but it clearly won't decrease
3687 # entropy.
3688 lines.append(message)
3689 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3690 stdin='\n'.join(lines))
3691 return 'I%s' % change_hash.strip()
3692
3693
wittman@chromium.org455dc922015-01-26 20:15:50 +00003694def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3695 """Computes the remote branch ref to use for the CL.
3696
3697 Args:
3698 remote (str): The git remote for the CL.
3699 remote_branch (str): The git remote branch for the CL.
3700 target_branch (str): The target branch specified by the user.
3701 pending_prefix (str): The pending prefix from the settings.
3702 """
3703 if not (remote and remote_branch):
3704 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003705
wittman@chromium.org455dc922015-01-26 20:15:50 +00003706 if target_branch:
3707 # Cannonicalize branch references to the equivalent local full symbolic
3708 # refs, which are then translated into the remote full symbolic refs
3709 # below.
3710 if '/' not in target_branch:
3711 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3712 else:
3713 prefix_replacements = (
3714 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3715 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3716 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3717 )
3718 match = None
3719 for regex, replacement in prefix_replacements:
3720 match = re.search(regex, target_branch)
3721 if match:
3722 remote_branch = target_branch.replace(match.group(0), replacement)
3723 break
3724 if not match:
3725 # This is a branch path but not one we recognize; use as-is.
3726 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003727 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3728 # Handle the refs that need to land in different refs.
3729 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003730
wittman@chromium.org455dc922015-01-26 20:15:50 +00003731 # Create the true path to the remote branch.
3732 # Does the following translation:
3733 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3734 # * refs/remotes/origin/master -> refs/heads/master
3735 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3736 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3737 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3738 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3739 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3740 'refs/heads/')
3741 elif remote_branch.startswith('refs/remotes/branch-heads'):
3742 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3743 # If a pending prefix exists then replace refs/ with it.
3744 if pending_prefix:
3745 remote_branch = remote_branch.replace('refs/', pending_prefix)
3746 return remote_branch
3747
3748
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003749def cleanup_list(l):
3750 """Fixes a list so that comma separated items are put as individual items.
3751
3752 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3753 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3754 """
3755 items = sum((i.split(',') for i in l), [])
3756 stripped_items = (i.strip() for i in items)
3757 return sorted(filter(None, stripped_items))
3758
3759
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003760@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003761def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003762 """Uploads the current changelist to codereview.
3763
3764 Can skip dependency patchset uploads for a branch by running:
3765 git config branch.branch_name.skip-deps-uploads True
3766 To unset run:
3767 git config --unset branch.branch_name.skip-deps-uploads
3768 Can also set the above globally by using the --global flag.
3769 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003770 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3771 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003772 parser.add_option('--bypass-watchlists', action='store_true',
3773 dest='bypass_watchlists',
3774 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003775 parser.add_option('-f', action='store_true', dest='force',
3776 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003777 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003778 parser.add_option('-b', '--bug',
3779 help='pre-populate the bug number(s) for this issue. '
3780 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003781 parser.add_option('--message-file', dest='message_file',
3782 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003783 parser.add_option('-t', dest='title',
3784 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003785 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003786 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003787 help='reviewer email addresses')
3788 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003789 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003790 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003791 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003792 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003793 parser.add_option('--emulate_svn_auto_props',
3794 '--emulate-svn-auto-props',
3795 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003796 dest="emulate_svn_auto_props",
3797 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003798 parser.add_option('-c', '--use-commit-queue', action='store_true',
3799 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003800 parser.add_option('--private', action='store_true',
3801 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003802 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003803 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003804 metavar='TARGET',
3805 help='Apply CL to remote ref TARGET. ' +
3806 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003807 parser.add_option('--squash', action='store_true',
3808 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003809 parser.add_option('--no-squash', action='store_true',
3810 help='Don\'t squash multiple commits into one ' +
3811 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003812 parser.add_option('--email', default=None,
3813 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003814 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3815 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003816 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3817 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003818 help='Send the patchset to do a CQ dry run right after '
3819 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003820 parser.add_option('--dependencies', action='store_true',
3821 help='Uploads CLs of all the local branches that depend on '
3822 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003823
rmistry@google.com2dd99862015-06-22 12:22:18 +00003824 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003825 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003826 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003827 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003828 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003829 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003830 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003831
sbc@chromium.org71437c02015-04-09 19:29:40 +00003832 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003833 return 1
3834
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003835 options.reviewers = cleanup_list(options.reviewers)
3836 options.cc = cleanup_list(options.cc)
3837
tandriib80458a2016-06-23 12:20:07 -07003838 if options.message_file:
3839 if options.message:
3840 parser.error('only one of --message and --message-file allowed.')
3841 options.message = gclient_utils.FileRead(options.message_file)
3842 options.message_file = None
3843
tandrii4d0545a2016-07-06 03:56:49 -07003844 if options.cq_dry_run and options.use_commit_queue:
3845 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3846
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003847 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3848 settings.GetIsGerrit()
3849
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003850 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003851 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003852
3853
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003854def IsSubmoduleMergeCommit(ref):
3855 # When submodules are added to the repo, we expect there to be a single
3856 # non-git-svn merge commit at remote HEAD with a signature comment.
3857 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003858 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003859 return RunGit(cmd) != ''
3860
3861
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003862def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003863 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003865 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3866 upstream and closes the issue automatically and atomically.
3867
3868 Otherwise (in case of Rietveld):
3869 Squashes branch into a single commit.
3870 Updates changelog with metadata (e.g. pointer to review).
3871 Pushes/dcommits the code upstream.
3872 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003873 """
3874 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3875 help='bypass upload presubmit hook')
3876 parser.add_option('-m', dest='message',
3877 help="override review description")
3878 parser.add_option('-f', action='store_true', dest='force',
3879 help="force yes to questions (don't prompt)")
3880 parser.add_option('-c', dest='contributor',
3881 help="external contributor for patch (appended to " +
3882 "description and used as author for git). Should be " +
3883 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003884 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003885 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003886 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003887 auth_config = auth.extract_auth_config_from_options(options)
3888
3889 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003890
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003891 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3892 if cl.IsGerrit():
3893 if options.message:
3894 # This could be implemented, but it requires sending a new patch to
3895 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3896 # Besides, Gerrit has the ability to change the commit message on submit
3897 # automatically, thus there is no need to support this option (so far?).
3898 parser.error('-m MESSAGE option is not supported for Gerrit.')
3899 if options.contributor:
3900 parser.error(
3901 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3902 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3903 'the contributor\'s "name <email>". If you can\'t upload such a '
3904 'commit for review, contact your repository admin and request'
3905 '"Forge-Author" permission.')
3906 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3907 options.verbose)
3908
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003909 current = cl.GetBranch()
3910 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3911 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print()
3913 print('Attempting to push branch %r into another local branch!' % current)
3914 print()
3915 print('Either reparent this branch on top of origin/master:')
3916 print(' git reparent-branch --root')
3917 print()
3918 print('OR run `git rebase-update` if you think the parent branch is ')
3919 print('already committed.')
3920 print()
3921 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003922 return 1
3923
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003924 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003925 # Default to merging against our best guess of the upstream branch.
3926 args = [cl.GetUpstreamBranch()]
3927
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003928 if options.contributor:
3929 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003930 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003931 return 1
3932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003933 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003934 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003935
sbc@chromium.org71437c02015-04-09 19:29:40 +00003936 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003937 return 1
3938
3939 # This rev-list syntax means "show all commits not in my branch that
3940 # are in base_branch".
3941 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3942 base_branch]).splitlines()
3943 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003944 print('Base branch "%s" has %d commits '
3945 'not in this branch.' % (base_branch, len(upstream_commits)))
3946 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003947 return 1
3948
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003949 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003950 svn_head = None
3951 if cmd == 'dcommit' or base_has_submodules:
3952 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3953 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003954
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003955 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003956 # If the base_head is a submodule merge commit, the first parent of the
3957 # base_head should be a git-svn commit, which is what we're interested in.
3958 base_svn_head = base_branch
3959 if base_has_submodules:
3960 base_svn_head += '^1'
3961
3962 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003963 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003964 print('This branch has %d additional commits not upstreamed yet.'
3965 % len(extra_commits.splitlines()))
3966 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3967 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003968 return 1
3969
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003970 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003971 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003972 author = None
3973 if options.contributor:
3974 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003975 hook_results = cl.RunHook(
3976 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003977 may_prompt=not options.force,
3978 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003979 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003980 if not hook_results.should_continue():
3981 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003982
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003983 # Check the tree status if the tree status URL is set.
3984 status = GetTreeStatus()
3985 if 'closed' == status:
3986 print('The tree is closed. Please wait for it to reopen. Use '
3987 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3988 return 1
3989 elif 'unknown' == status:
3990 print('Unable to determine tree status. Please verify manually and '
3991 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3992 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003993
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003994 change_desc = ChangeDescription(options.message)
3995 if not change_desc.description and cl.GetIssue():
3996 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003998 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003999 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004000 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004001 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004002 print('No description set.')
4003 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004004 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004006 # Keep a separate copy for the commit message, because the commit message
4007 # contains the link to the Rietveld issue, while the Rietveld message contains
4008 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004009 # Keep a separate copy for the commit message.
4010 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004011 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004012
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004013 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004014 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004015 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004016 # after it. Add a period on a new line to circumvent this. Also add a space
4017 # before the period to make sure that Gitiles continues to correctly resolve
4018 # the URL.
4019 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004020 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004021 commit_desc.append_footer('Patch from %s.' % options.contributor)
4022
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004023 print('Description:')
4024 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004025
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004026 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004027 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004028 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004029
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004030 # We want to squash all this branch's commits into one commit with the proper
4031 # description. We do this by doing a "reset --soft" to the base branch (which
4032 # keeps the working copy the same), then dcommitting that. If origin/master
4033 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4034 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004035 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004036 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4037 # Delete the branches if they exist.
4038 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4039 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4040 result = RunGitWithCode(showref_cmd)
4041 if result[0] == 0:
4042 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004043
4044 # We might be in a directory that's present in this branch but not in the
4045 # trunk. Move up to the top of the tree so that git commands that expect a
4046 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004047 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048 if rel_base_path:
4049 os.chdir(rel_base_path)
4050
4051 # Stuff our change into the merge branch.
4052 # We wrap in a try...finally block so if anything goes wrong,
4053 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004054 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004055 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004056 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004057 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004058 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004059 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004060 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004061 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004062 RunGit(
4063 [
4064 'commit', '--author', options.contributor,
4065 '-m', commit_desc.description,
4066 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004067 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004068 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004069 if base_has_submodules:
4070 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4071 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4072 RunGit(['checkout', CHERRY_PICK_BRANCH])
4073 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004074 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004075 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004076 mirror = settings.GetGitMirror(remote)
4077 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004078 pending_prefix = settings.GetPendingRefPrefix()
4079 if not pending_prefix or branch.startswith(pending_prefix):
4080 # If not using refs/pending/heads/* at all, or target ref is already set
4081 # to pending, then push to the target ref directly.
4082 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004083 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004084 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004085 else:
4086 # Cherry-pick the change on top of pending ref and then push it.
4087 assert branch.startswith('refs/'), branch
4088 assert pending_prefix[-1] == '/', pending_prefix
4089 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004090 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004091 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004092 if retcode == 0:
4093 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004094 else:
4095 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004096 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004097 'svn', 'dcommit',
4098 '-C%s' % options.similarity,
4099 '--no-rebase', '--rmdir',
4100 ]
4101 if settings.GetForceHttpsCommitUrl():
4102 # Allow forcing https commit URLs for some projects that don't allow
4103 # committing to http URLs (like Google Code).
4104 remote_url = cl.GetGitSvnRemoteUrl()
4105 if urlparse.urlparse(remote_url).scheme == 'http':
4106 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004107 cmd_args.append('--commit-url=%s' % remote_url)
4108 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004109 if 'Committed r' in output:
4110 revision = re.match(
4111 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4112 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004113 finally:
4114 # And then swap back to the original branch and clean up.
4115 RunGit(['checkout', '-q', cl.GetBranch()])
4116 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004117 if base_has_submodules:
4118 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004119
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004120 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004121 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004122 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004123
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004124 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004125 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004126 try:
4127 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4128 # We set pushed_to_pending to False, since it made it all the way to the
4129 # real ref.
4130 pushed_to_pending = False
4131 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004132 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004134 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004135 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004136 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004137 if not to_pending:
4138 if viewvc_url and revision:
4139 change_desc.append_footer(
4140 'Committed: %s%s' % (viewvc_url, revision))
4141 elif revision:
4142 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print('Closing issue '
4144 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004145 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004146 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004147 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004148 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004149 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004150 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004151 if options.bypass_hooks:
4152 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4153 else:
4154 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004155 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004156
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004157 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004158 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004159 print('The commit is in the pending queue (%s).' % pending_ref)
4160 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4161 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004162
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004163 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4164 if os.path.isfile(hook):
4165 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004166
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004167 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004168
4169
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004170def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004171 print()
4172 print('Waiting for commit to be landed on %s...' % real_ref)
4173 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004174 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4175 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004176 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004177
4178 loop = 0
4179 while True:
4180 sys.stdout.write('fetching (%d)... \r' % loop)
4181 sys.stdout.flush()
4182 loop += 1
4183
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004184 if mirror:
4185 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004186 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4187 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4188 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4189 for commit in commits.splitlines():
4190 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004191 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004192 return commit
4193
4194 current_rev = to_rev
4195
4196
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004197def PushToGitPending(remote, pending_ref, upstream_ref):
4198 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4199
4200 Returns:
4201 (retcode of last operation, output log of last operation).
4202 """
4203 assert pending_ref.startswith('refs/'), pending_ref
4204 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4205 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4206 code = 0
4207 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004208 max_attempts = 3
4209 attempts_left = max_attempts
4210 while attempts_left:
4211 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004212 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004213 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004214
4215 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004216 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004217 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004218 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004219 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004220 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004221 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004222 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004223 continue
4224
4225 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004226 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004227 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004228 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004229 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004230 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4231 'the following files have merge conflicts:' % pending_ref)
4232 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4233 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004234 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004235 return code, out
4236
4237 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004238 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004239 code, out = RunGitWithCode(
4240 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4241 if code == 0:
4242 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004243 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004244 return code, out
4245
vapiera7fbd5a2016-06-16 09:17:49 -07004246 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004247 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004248 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004249 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004250 print('Fatal push error. Make sure your .netrc credentials and git '
4251 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004252 return code, out
4253
vapiera7fbd5a2016-06-16 09:17:49 -07004254 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004255 return code, out
4256
4257
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004258def IsFatalPushFailure(push_stdout):
4259 """True if retrying push won't help."""
4260 return '(prohibited by Gerrit)' in push_stdout
4261
4262
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004263@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004264def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004265 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004267 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004268 # If it looks like previous commits were mirrored with git-svn.
4269 message = """This repository appears to be a git-svn mirror, but no
4270upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4271 else:
4272 message = """This doesn't appear to be an SVN repository.
4273If your project has a true, writeable git repository, you probably want to run
4274'git cl land' instead.
4275If your project has a git mirror of an upstream SVN master, you probably need
4276to run 'git svn init'.
4277
4278Using the wrong command might cause your commit to appear to succeed, and the
4279review to be closed, without actually landing upstream. If you choose to
4280proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004281 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004282 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004283 # TODO(tandrii): kill this post SVN migration with
4284 # https://codereview.chromium.org/2076683002
4285 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4286 'Please let us know of this project you are committing to:'
4287 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004288 return SendUpstream(parser, args, 'dcommit')
4289
4290
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004291@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004292def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004293 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004294 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004295 print('This appears to be an SVN repository.')
4296 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004297 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004298 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004299 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004300
4301
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004302@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004303def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004304 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305 parser.add_option('-b', dest='newbranch',
4306 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004307 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004308 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004309 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4310 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004311 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004312 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004313 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004314 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004316 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004317
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004318
4319 group = optparse.OptionGroup(
4320 parser,
4321 'Options for continuing work on the current issue uploaded from a '
4322 'different clone (e.g. different machine). Must be used independently '
4323 'from the other options. No issue number should be specified, and the '
4324 'branch must have an issue number associated with it')
4325 group.add_option('--reapply', action='store_true', dest='reapply',
4326 help='Reset the branch and reapply the issue.\n'
4327 'CAUTION: This will undo any local changes in this '
4328 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004329
4330 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004331 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004332 parser.add_option_group(group)
4333
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004334 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004335 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004336 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004337 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004338 auth_config = auth.extract_auth_config_from_options(options)
4339
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004340
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004341 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004342 if options.newbranch:
4343 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004344 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004345 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004346
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004347 cl = Changelist(auth_config=auth_config,
4348 codereview=options.forced_codereview)
4349 if not cl.GetIssue():
4350 parser.error('current branch must have an associated issue')
4351
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004352 upstream = cl.GetUpstreamBranch()
4353 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004354 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004355
4356 RunGit(['reset', '--hard', upstream])
4357 if options.pull:
4358 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004359
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004360 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4361 options.directory)
4362
4363 if len(args) != 1 or not args[0]:
4364 parser.error('Must specify issue number or url')
4365
4366 # We don't want uncommitted changes mixed up with the patch.
4367 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004368 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004370 if options.newbranch:
4371 if options.force:
4372 RunGit(['branch', '-D', options.newbranch],
4373 stderr=subprocess2.PIPE, error_ok=True)
4374 RunGit(['new-branch', options.newbranch])
4375
4376 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4377
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004378 if cl.IsGerrit():
4379 if options.reject:
4380 parser.error('--reject is not supported with Gerrit codereview.')
4381 if options.nocommit:
4382 parser.error('--nocommit is not supported with Gerrit codereview.')
4383 if options.directory:
4384 parser.error('--directory is not supported with Gerrit codereview.')
4385
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004386 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004387 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004388
4389
4390def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004391 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004392 # Provide a wrapper for git svn rebase to help avoid accidental
4393 # git svn dcommit.
4394 # It's the only command that doesn't use parser at all since we just defer
4395 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004396
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004397 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004398
4399
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004400def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004401 """Fetches the tree status and returns either 'open', 'closed',
4402 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004403 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004404 if url:
4405 status = urllib2.urlopen(url).read().lower()
4406 if status.find('closed') != -1 or status == '0':
4407 return 'closed'
4408 elif status.find('open') != -1 or status == '1':
4409 return 'open'
4410 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004411 return 'unset'
4412
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004413
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004414def GetTreeStatusReason():
4415 """Fetches the tree status from a json url and returns the message
4416 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004417 url = settings.GetTreeStatusUrl()
4418 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004419 connection = urllib2.urlopen(json_url)
4420 status = json.loads(connection.read())
4421 connection.close()
4422 return status['message']
4423
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004424
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004425def GetBuilderMaster(bot_list):
4426 """For a given builder, fetch the master from AE if available."""
4427 map_url = 'https://builders-map.appspot.com/'
4428 try:
4429 master_map = json.load(urllib2.urlopen(map_url))
4430 except urllib2.URLError as e:
4431 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4432 (map_url, e))
4433 except ValueError as e:
4434 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4435 if not master_map:
4436 return None, 'Failed to build master map.'
4437
4438 result_master = ''
4439 for bot in bot_list:
4440 builder = bot.split(':', 1)[0]
4441 master_list = master_map.get(builder, [])
4442 if not master_list:
4443 return None, ('No matching master for builder %s.' % builder)
4444 elif len(master_list) > 1:
4445 return None, ('The builder name %s exists in multiple masters %s.' %
4446 (builder, master_list))
4447 else:
4448 cur_master = master_list[0]
4449 if not result_master:
4450 result_master = cur_master
4451 elif result_master != cur_master:
4452 return None, 'The builders do not belong to the same master.'
4453 return result_master, None
4454
4455
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004456def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004457 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004458 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004459 status = GetTreeStatus()
4460 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004461 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004462 return 2
4463
vapiera7fbd5a2016-06-16 09:17:49 -07004464 print('The tree is %s' % status)
4465 print()
4466 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004467 if status != 'open':
4468 return 1
4469 return 0
4470
4471
maruel@chromium.org15192402012-09-06 12:38:29 +00004472def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004473 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004474 group = optparse.OptionGroup(parser, "Try job options")
4475 group.add_option(
4476 "-b", "--bot", action="append",
4477 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4478 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004479 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004480 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004481 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004482 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004483 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004484 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004485 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004486 "-r", "--revision",
4487 help="Revision to use for the try job; default: the "
4488 "revision will be determined by the try server; see "
4489 "its waterfall for more info")
4490 group.add_option(
4491 "-c", "--clobber", action="store_true", default=False,
4492 help="Force a clobber before building; e.g. don't do an "
4493 "incremental build")
4494 group.add_option(
4495 "--project",
4496 help="Override which project to use. Projects are defined "
4497 "server-side to define what default bot set to use")
4498 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004499 "-p", "--property", dest="properties", action="append", default=[],
4500 help="Specify generic properties in the form -p key1=value1 -p "
4501 "key2=value2 etc (buildbucket only). The value will be treated as "
4502 "json if decodable, or as string otherwise.")
4503 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004504 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004505 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004506 "--use-rietveld", action="store_true", default=False,
4507 help="Use Rietveld to trigger try jobs.")
4508 group.add_option(
4509 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4510 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004511 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004512 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004513 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004514 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004515
machenbach@chromium.org45453142015-09-15 08:45:22 +00004516 if options.use_rietveld and options.properties:
4517 parser.error('Properties can only be specified with buildbucket')
4518
4519 # Make sure that all properties are prop=value pairs.
4520 bad_params = [x for x in options.properties if '=' not in x]
4521 if bad_params:
4522 parser.error('Got properties with missing "=": %s' % bad_params)
4523
maruel@chromium.org15192402012-09-06 12:38:29 +00004524 if args:
4525 parser.error('Unknown arguments: %s' % args)
4526
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004527 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004528 if not cl.GetIssue():
4529 parser.error('Need to upload first')
4530
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004531 if cl.IsGerrit():
4532 parser.error(
4533 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4534 'If your project has Commit Queue, dry run is a workaround:\n'
4535 ' git cl set-commit --dry-run')
4536 # Code below assumes Rietveld issue.
4537 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4538
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004539 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004540 if props.get('closed'):
4541 parser.error('Cannot send tryjobs for a closed CL')
4542
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004543 if props.get('private'):
4544 parser.error('Cannot use trybots with private issue')
4545
maruel@chromium.org15192402012-09-06 12:38:29 +00004546 if not options.name:
4547 options.name = cl.GetBranch()
4548
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004549 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004550 options.master, err_msg = GetBuilderMaster(options.bot)
4551 if err_msg:
4552 parser.error('Tryserver master cannot be found because: %s\n'
4553 'Please manually specify the tryserver master'
4554 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004555
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004556 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004557 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004558 if not options.bot:
4559 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004560
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004561 # Get try masters from PRESUBMIT.py files.
4562 masters = presubmit_support.DoGetTryMasters(
4563 change,
4564 change.LocalPaths(),
4565 settings.GetRoot(),
4566 None,
4567 None,
4568 options.verbose,
4569 sys.stdout)
4570 if masters:
4571 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004572
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004573 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4574 options.bot = presubmit_support.DoGetTrySlaves(
4575 change,
4576 change.LocalPaths(),
4577 settings.GetRoot(),
4578 None,
4579 None,
4580 options.verbose,
4581 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004582
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004583 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004584 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004585
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004586 builders_and_tests = {}
4587 # TODO(machenbach): The old style command-line options don't support
4588 # multiple try masters yet.
4589 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4590 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4591
4592 for bot in old_style:
4593 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004594 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004595 elif ',' in bot:
4596 parser.error('Specify one bot per --bot flag')
4597 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004598 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004599
4600 for bot, tests in new_style:
4601 builders_and_tests.setdefault(bot, []).extend(tests)
4602
4603 # Return a master map with one master to be backwards compatible. The
4604 # master name defaults to an empty string, which will cause the master
4605 # not to be set on rietveld (deprecated).
4606 return {options.master: builders_and_tests}
4607
4608 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004609 if not masters:
4610 # Default to triggering Dry Run (see http://crbug.com/625697).
4611 if options.verbose:
4612 print('git cl try with no bots now defaults to CQ Dry Run.')
4613 try:
4614 cl.SetCQState(_CQState.DRY_RUN)
4615 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4616 return 0
4617 except KeyboardInterrupt:
4618 raise
4619 except:
4620 print('WARNING: failed to trigger CQ Dry Run.\n'
4621 'Either:\n'
4622 ' * your project has no CQ\n'
4623 ' * you don\'t have permission to trigger Dry Run\n'
4624 ' * bug in this code (see stack trace below).\n'
4625 'Consider specifying which bots to trigger manually '
4626 'or asking your project owners for permissions '
4627 'or contacting Chrome Infrastructure team at '
4628 'https://www.chromium.org/infra\n\n')
4629 # Still raise exception so that stack trace is printed.
4630 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004631
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004632 for builders in masters.itervalues():
4633 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004634 print('ERROR You are trying to send a job to a triggered bot. This type '
4635 'of bot requires an\ninitial job from a parent (usually a builder).'
4636 ' Instead send your job to the parent.\n'
4637 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004638 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004639
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004640 patchset = cl.GetMostRecentPatchset()
4641 if patchset and patchset != cl.GetPatchset():
4642 print(
4643 '\nWARNING Mismatch between local config and server. Did a previous '
4644 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4645 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004646 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004647 try:
4648 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4649 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004650 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004651 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004652 except Exception as e:
4653 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004654 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4655 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004656 return 1
4657 else:
4658 try:
4659 cl.RpcServer().trigger_distributed_try_jobs(
4660 cl.GetIssue(), patchset, options.name, options.clobber,
4661 options.revision, masters)
4662 except urllib2.HTTPError as e:
4663 if e.code == 404:
4664 print('404 from rietveld; '
4665 'did you mean to use "git try" instead of "git cl try"?')
4666 return 1
4667 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004668
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004669 for (master, builders) in sorted(masters.iteritems()):
4670 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004671 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004672 length = max(len(builder) for builder in builders)
4673 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004674 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004675 return 0
4676
4677
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004678def CMDtry_results(parser, args):
4679 group = optparse.OptionGroup(parser, "Try job results options")
4680 group.add_option(
4681 "-p", "--patchset", type=int, help="patchset number if not current.")
4682 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004683 "--print-master", action='store_true', help="print master name as well.")
4684 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004685 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004686 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004687 group.add_option(
4688 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4689 help="Host of buildbucket. The default host is %default.")
4690 parser.add_option_group(group)
4691 auth.add_auth_options(parser)
4692 options, args = parser.parse_args(args)
4693 if args:
4694 parser.error('Unrecognized args: %s' % ' '.join(args))
4695
4696 auth_config = auth.extract_auth_config_from_options(options)
4697 cl = Changelist(auth_config=auth_config)
4698 if not cl.GetIssue():
4699 parser.error('Need to upload first')
4700
4701 if not options.patchset:
4702 options.patchset = cl.GetMostRecentPatchset()
4703 if options.patchset and options.patchset != cl.GetPatchset():
4704 print(
4705 '\nWARNING Mismatch between local config and server. Did a previous '
4706 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4707 'Continuing using\npatchset %s.\n' % options.patchset)
4708 try:
4709 jobs = fetch_try_jobs(auth_config, cl, options)
4710 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004711 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004712 return 1
4713 except Exception as e:
4714 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004715 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4716 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004717 return 1
4718 print_tryjobs(options, jobs)
4719 return 0
4720
4721
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004722@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004723def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004724 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004725 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004726 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004727 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004728
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004730 if args:
4731 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004732 branch = cl.GetBranch()
4733 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004734 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004735 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004736
4737 # Clear configured merge-base, if there is one.
4738 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004739 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004740 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004741 return 0
4742
4743
thestig@chromium.org00858c82013-12-02 23:08:03 +00004744def CMDweb(parser, args):
4745 """Opens the current CL in the web browser."""
4746 _, args = parser.parse_args(args)
4747 if args:
4748 parser.error('Unrecognized args: %s' % ' '.join(args))
4749
4750 issue_url = Changelist().GetIssueURL()
4751 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004752 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004753 return 1
4754
4755 webbrowser.open(issue_url)
4756 return 0
4757
4758
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004759def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004760 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004761 parser.add_option('-d', '--dry-run', action='store_true',
4762 help='trigger in dry run mode')
4763 parser.add_option('-c', '--clear', action='store_true',
4764 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004765 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004766 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004767 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004768 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004769 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004770 if args:
4771 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004772 if options.dry_run and options.clear:
4773 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4774
iannuccie53c9352016-08-17 14:40:40 -07004775 cl = Changelist(auth_config=auth_config, issue=options.issue,
4776 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004777 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004778 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004779 elif options.dry_run:
4780 state = _CQState.DRY_RUN
4781 else:
4782 state = _CQState.COMMIT
4783 if not cl.GetIssue():
4784 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004785 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004786 return 0
4787
4788
groby@chromium.org411034a2013-02-26 15:12:01 +00004789def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004790 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004791 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004792 auth.add_auth_options(parser)
4793 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004794 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004795 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004796 if args:
4797 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004798 cl = Changelist(auth_config=auth_config, issue=options.issue,
4799 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004800 # Ensure there actually is an issue to close.
4801 cl.GetDescription()
4802 cl.CloseIssue()
4803 return 0
4804
4805
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004806def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004807 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004808 auth.add_auth_options(parser)
4809 options, args = parser.parse_args(args)
4810 auth_config = auth.extract_auth_config_from_options(options)
4811 if args:
4812 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004813
4814 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004815 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004816 # Staged changes would be committed along with the patch from last
4817 # upload, hence counted toward the "last upload" side in the final
4818 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004819 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004820 return 1
4821
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004822 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004823 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004824 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004825 if not issue:
4826 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004827 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004828 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004829
4830 # Create a new branch based on the merge-base
4831 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004832 # Clear cached branch in cl object, to avoid overwriting original CL branch
4833 # properties.
4834 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004835 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004836 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004837 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004838 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004839 return rtn
4840
wychen@chromium.org06928532015-02-03 02:11:29 +00004841 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004842 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004843 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004844 finally:
4845 RunGit(['checkout', '-q', branch])
4846 RunGit(['branch', '-D', TMP_BRANCH])
4847
4848 return 0
4849
4850
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004851def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004852 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004853 parser.add_option(
4854 '--no-color',
4855 action='store_true',
4856 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004857 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004858 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004859 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004860
4861 author = RunGit(['config', 'user.email']).strip() or None
4862
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004863 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004864
4865 if args:
4866 if len(args) > 1:
4867 parser.error('Unknown args')
4868 base_branch = args[0]
4869 else:
4870 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004871 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004872
4873 change = cl.GetChange(base_branch, None)
4874 return owners_finder.OwnersFinder(
4875 [f.LocalPath() for f in
4876 cl.GetChange(base_branch, None).AffectedFiles()],
4877 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004878 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004879 disable_color=options.no_color).run()
4880
4881
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004882def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004883 """Generates a diff command."""
4884 # Generate diff for the current branch's changes.
4885 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4886 upstream_commit, '--' ]
4887
4888 if args:
4889 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004890 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004891 diff_cmd.append(arg)
4892 else:
4893 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004894
4895 return diff_cmd
4896
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004897def MatchingFileType(file_name, extensions):
4898 """Returns true if the file name ends with one of the given extensions."""
4899 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004900
enne@chromium.org555cfe42014-01-29 18:21:39 +00004901@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004902def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004903 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004904 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004905 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004906 parser.add_option('--full', action='store_true',
4907 help='Reformat the full content of all touched files')
4908 parser.add_option('--dry-run', action='store_true',
4909 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004910 parser.add_option('--python', action='store_true',
4911 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004912 parser.add_option('--diff', action='store_true',
4913 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004914 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004915
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004916 # git diff generates paths against the root of the repository. Change
4917 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004918 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004919 if rel_base_path:
4920 os.chdir(rel_base_path)
4921
digit@chromium.org29e47272013-05-17 17:01:46 +00004922 # Grab the merge-base commit, i.e. the upstream commit of the current
4923 # branch when it was created or the last time it was rebased. This is
4924 # to cover the case where the user may have called "git fetch origin",
4925 # moving the origin branch to a newer commit, but hasn't rebased yet.
4926 upstream_commit = None
4927 cl = Changelist()
4928 upstream_branch = cl.GetUpstreamBranch()
4929 if upstream_branch:
4930 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4931 upstream_commit = upstream_commit.strip()
4932
4933 if not upstream_commit:
4934 DieWithError('Could not find base commit for this branch. '
4935 'Are you in detached state?')
4936
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004937 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4938 diff_output = RunGit(changed_files_cmd)
4939 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004940 # Filter out files deleted by this CL
4941 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004942
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004943 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4944 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4945 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004946 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004947
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004948 top_dir = os.path.normpath(
4949 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4950
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004951 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4952 # formatted. This is used to block during the presubmit.
4953 return_value = 0
4954
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004955 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004956 # Locate the clang-format binary in the checkout
4957 try:
4958 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004959 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004960 DieWithError(e)
4961
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004962 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004963 cmd = [clang_format_tool]
4964 if not opts.dry_run and not opts.diff:
4965 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004966 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004967 if opts.diff:
4968 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004969 else:
4970 env = os.environ.copy()
4971 env['PATH'] = str(os.path.dirname(clang_format_tool))
4972 try:
4973 script = clang_format.FindClangFormatScriptInChromiumTree(
4974 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004975 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004976 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004977
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004978 cmd = [sys.executable, script, '-p0']
4979 if not opts.dry_run and not opts.diff:
4980 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004981
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004982 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4983 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004984
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004985 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4986 if opts.diff:
4987 sys.stdout.write(stdout)
4988 if opts.dry_run and len(stdout) > 0:
4989 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004990
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004991 # Similar code to above, but using yapf on .py files rather than clang-format
4992 # on C/C++ files
4993 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004994 yapf_tool = gclient_utils.FindExecutable('yapf')
4995 if yapf_tool is None:
4996 DieWithError('yapf not found in PATH')
4997
4998 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004999 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005000 cmd = [yapf_tool]
5001 if not opts.dry_run and not opts.diff:
5002 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005003 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005004 if opts.diff:
5005 sys.stdout.write(stdout)
5006 else:
5007 # TODO(sbc): yapf --lines mode still has some issues.
5008 # https://github.com/google/yapf/issues/154
5009 DieWithError('--python currently only works with --full')
5010
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005011 # Dart's formatter does not have the nice property of only operating on
5012 # modified chunks, so hard code full.
5013 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005014 try:
5015 command = [dart_format.FindDartFmtToolInChromiumTree()]
5016 if not opts.dry_run and not opts.diff:
5017 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005018 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005019
ppi@chromium.org6593d932016-03-03 15:41:15 +00005020 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005021 if opts.dry_run and stdout:
5022 return_value = 2
5023 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005024 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5025 'found in this checkout. Files in other languages are still '
5026 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005027
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005028 # Format GN build files. Always run on full build files for canonical form.
5029 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005030 cmd = ['gn', 'format' ]
5031 if opts.dry_run or opts.diff:
5032 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005033 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005034 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5035 shell=sys.platform == 'win32',
5036 cwd=top_dir)
5037 if opts.dry_run and gn_ret == 2:
5038 return_value = 2 # Not formatted.
5039 elif opts.diff and gn_ret == 2:
5040 # TODO this should compute and print the actual diff.
5041 print("This change has GN build file diff for " + gn_diff_file)
5042 elif gn_ret != 0:
5043 # For non-dry run cases (and non-2 return values for dry-run), a
5044 # nonzero error code indicates a failure, probably because the file
5045 # doesn't parse.
5046 DieWithError("gn format failed on " + gn_diff_file +
5047 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005048
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005049 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005050
5051
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005052@subcommand.usage('<codereview url or issue id>')
5053def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005054 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005055 _, args = parser.parse_args(args)
5056
5057 if len(args) != 1:
5058 parser.print_help()
5059 return 1
5060
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005061 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005062 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005063 parser.print_help()
5064 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005065 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005066
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005067 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005068 output = RunGit(['config', '--local', '--get-regexp',
5069 r'branch\..*\.%s' % issueprefix],
5070 error_ok=True)
5071 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005072 if issue == target_issue:
5073 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005074
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005075 branches = []
5076 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00005077 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005078 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005079 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005080 return 1
5081 if len(branches) == 1:
5082 RunGit(['checkout', branches[0]])
5083 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005084 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005085 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005086 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005087 which = raw_input('Choose by index: ')
5088 try:
5089 RunGit(['checkout', branches[int(which)]])
5090 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005091 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005092 return 1
5093
5094 return 0
5095
5096
maruel@chromium.org29404b52014-09-08 22:58:00 +00005097def CMDlol(parser, args):
5098 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005099 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005100 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5101 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5102 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005103 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005104 return 0
5105
5106
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005107class OptionParser(optparse.OptionParser):
5108 """Creates the option parse and add --verbose support."""
5109 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005110 optparse.OptionParser.__init__(
5111 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005112 self.add_option(
5113 '-v', '--verbose', action='count', default=0,
5114 help='Use 2 times for more debugging info')
5115
5116 def parse_args(self, args=None, values=None):
5117 options, args = optparse.OptionParser.parse_args(self, args, values)
5118 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5119 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5120 return options, args
5121
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005122
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005123def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005124 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005125 print('\nYour python version %s is unsupported, please upgrade.\n' %
5126 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005127 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005128
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005129 # Reload settings.
5130 global settings
5131 settings = Settings()
5132
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005133 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005134 dispatcher = subcommand.CommandDispatcher(__name__)
5135 try:
5136 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005137 except auth.AuthenticationError as e:
5138 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005139 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005140 if e.code != 500:
5141 raise
5142 DieWithError(
5143 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5144 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005145 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005146
5147
5148if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005149 # These affect sys.stdout so do it outside of main() to simplify mocks in
5150 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005151 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005152 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005153 try:
5154 sys.exit(main(sys.argv[1:]))
5155 except KeyboardInterrupt:
5156 sys.stderr.write('interrupted\n')
5157 sys.exit(1)