blob: 42af2a77f2bc6d09e8b9b2e02255e88e6644e9fd [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.
tandrii4d895502016-08-18 08:26:19 -07001464 # Note that child method defines __getattr__ as well, and forwards it here,
1465 # because _RietveldChangelistImpl is not cleaned up yet, and given
1466 # deprecation of Rietveld, it should probably be just removed.
1467 # Until that time, avoid infinite recursion by bypassing __getattr__
1468 # of implementation class.
1469 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001470
1471
1472class _ChangelistCodereviewBase(object):
1473 """Abstract base class encapsulating codereview specifics of a changelist."""
1474 def __init__(self, changelist):
1475 self._changelist = changelist # instance of Changelist
1476
1477 def __getattr__(self, attr):
1478 # Forward methods to changelist.
1479 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1480 # _RietveldChangelistImpl to avoid this hack?
1481 return getattr(self._changelist, attr)
1482
1483 def GetStatus(self):
1484 """Apply a rough heuristic to give a simple summary of an issue's review
1485 or CQ status, assuming adherence to a common workflow.
1486
1487 Returns None if no issue for this branch, or specific string keywords.
1488 """
1489 raise NotImplementedError()
1490
1491 def GetCodereviewServer(self):
1492 """Returns server URL without end slash, like "https://codereview.com"."""
1493 raise NotImplementedError()
1494
1495 def FetchDescription(self):
1496 """Fetches and returns description from the codereview server."""
1497 raise NotImplementedError()
1498
1499 def GetCodereviewServerSetting(self):
1500 """Returns git config setting for the codereview server."""
1501 raise NotImplementedError()
1502
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001503 @classmethod
1504 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001505 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001506
1507 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001508 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001509 """Returns name of git config setting which stores issue number for a given
1510 branch."""
1511 raise NotImplementedError()
1512
1513 def PatchsetSetting(self):
1514 """Returns name of git config setting which stores issue number."""
1515 raise NotImplementedError()
1516
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001517 def _PostUnsetIssueProperties(self):
1518 """Which branch-specific properties to erase when unsettin issue."""
1519 raise NotImplementedError()
1520
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001521 def GetRieveldObjForPresubmit(self):
1522 # This is an unfortunate Rietveld-embeddedness in presubmit.
1523 # For non-Rietveld codereviews, this probably should return a dummy object.
1524 raise NotImplementedError()
1525
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001526 def GetGerritObjForPresubmit(self):
1527 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1528 return None
1529
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001530 def UpdateDescriptionRemote(self, description):
1531 """Update the description on codereview site."""
1532 raise NotImplementedError()
1533
1534 def CloseIssue(self):
1535 """Closes the issue."""
1536 raise NotImplementedError()
1537
1538 def GetApprovingReviewers(self):
1539 """Returns a list of reviewers approving the change.
1540
1541 Note: not necessarily committers.
1542 """
1543 raise NotImplementedError()
1544
1545 def GetMostRecentPatchset(self):
1546 """Returns the most recent patchset number from the codereview site."""
1547 raise NotImplementedError()
1548
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001549 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1550 directory):
1551 """Fetches and applies the issue.
1552
1553 Arguments:
1554 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1555 reject: if True, reject the failed patch instead of switching to 3-way
1556 merge. Rietveld only.
1557 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1558 only.
1559 directory: switch to directory before applying the patch. Rietveld only.
1560 """
1561 raise NotImplementedError()
1562
1563 @staticmethod
1564 def ParseIssueURL(parsed_url):
1565 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1566 failed."""
1567 raise NotImplementedError()
1568
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001569 def EnsureAuthenticated(self, force):
1570 """Best effort check that user is authenticated with codereview server.
1571
1572 Arguments:
1573 force: whether to skip confirmation questions.
1574 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001575 raise NotImplementedError()
1576
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001577 def CMDUploadChange(self, options, args, change):
1578 """Uploads a change to codereview."""
1579 raise NotImplementedError()
1580
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001581 def SetCQState(self, new_state):
1582 """Update the CQ state for latest patchset.
1583
1584 Issue must have been already uploaded and known.
1585 """
1586 raise NotImplementedError()
1587
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001588
1589class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1590 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1591 super(_RietveldChangelistImpl, self).__init__(changelist)
1592 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001593 if not rietveld_server:
1594 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001595
1596 self._rietveld_server = rietveld_server
1597 self._auth_config = auth_config
1598 self._props = None
1599 self._rpc_server = None
1600
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001601 def GetCodereviewServer(self):
1602 if not self._rietveld_server:
1603 # If we're on a branch then get the server potentially associated
1604 # with that branch.
1605 if self.GetIssue():
1606 rietveld_server_setting = self.GetCodereviewServerSetting()
1607 if rietveld_server_setting:
1608 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1609 ['config', rietveld_server_setting], error_ok=True).strip())
1610 if not self._rietveld_server:
1611 self._rietveld_server = settings.GetDefaultServerUrl()
1612 return self._rietveld_server
1613
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001614 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001615 """Best effort check that user is authenticated with Rietveld server."""
1616 if self._auth_config.use_oauth2:
1617 authenticator = auth.get_authenticator_for_host(
1618 self.GetCodereviewServer(), self._auth_config)
1619 if not authenticator.has_cached_credentials():
1620 raise auth.LoginRequiredError(self.GetCodereviewServer())
1621
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001622 def FetchDescription(self):
1623 issue = self.GetIssue()
1624 assert issue
1625 try:
1626 return self.RpcServer().get_description(issue).strip()
1627 except urllib2.HTTPError as e:
1628 if e.code == 404:
1629 DieWithError(
1630 ('\nWhile fetching the description for issue %d, received a '
1631 '404 (not found)\n'
1632 'error. It is likely that you deleted this '
1633 'issue on the server. If this is the\n'
1634 'case, please run\n\n'
1635 ' git cl issue 0\n\n'
1636 'to clear the association with the deleted issue. Then run '
1637 'this command again.') % issue)
1638 else:
1639 DieWithError(
1640 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1641 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001642 print('Warning: Failed to retrieve CL description due to network '
1643 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001644 return ''
1645
1646 def GetMostRecentPatchset(self):
1647 return self.GetIssueProperties()['patchsets'][-1]
1648
1649 def GetPatchSetDiff(self, issue, patchset):
1650 return self.RpcServer().get(
1651 '/download/issue%s_%s.diff' % (issue, patchset))
1652
1653 def GetIssueProperties(self):
1654 if self._props is None:
1655 issue = self.GetIssue()
1656 if not issue:
1657 self._props = {}
1658 else:
1659 self._props = self.RpcServer().get_issue_properties(issue, True)
1660 return self._props
1661
1662 def GetApprovingReviewers(self):
1663 return get_approving_reviewers(self.GetIssueProperties())
1664
1665 def AddComment(self, message):
1666 return self.RpcServer().add_comment(self.GetIssue(), message)
1667
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001668 def GetStatus(self):
1669 """Apply a rough heuristic to give a simple summary of an issue's review
1670 or CQ status, assuming adherence to a common workflow.
1671
1672 Returns None if no issue for this branch, or one of the following keywords:
1673 * 'error' - error from review tool (including deleted issues)
1674 * 'unsent' - not sent for review
1675 * 'waiting' - waiting for review
1676 * 'reply' - waiting for owner to reply to review
1677 * 'lgtm' - LGTM from at least one approved reviewer
1678 * 'commit' - in the commit queue
1679 * 'closed' - closed
1680 """
1681 if not self.GetIssue():
1682 return None
1683
1684 try:
1685 props = self.GetIssueProperties()
1686 except urllib2.HTTPError:
1687 return 'error'
1688
1689 if props.get('closed'):
1690 # Issue is closed.
1691 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001692 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001693 # Issue is in the commit queue.
1694 return 'commit'
1695
1696 try:
1697 reviewers = self.GetApprovingReviewers()
1698 except urllib2.HTTPError:
1699 return 'error'
1700
1701 if reviewers:
1702 # Was LGTM'ed.
1703 return 'lgtm'
1704
1705 messages = props.get('messages') or []
1706
tandrii9d2c7a32016-06-22 03:42:45 -07001707 # Skip CQ messages that don't require owner's action.
1708 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1709 if 'Dry run:' in messages[-1]['text']:
1710 messages.pop()
1711 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1712 # This message always follows prior messages from CQ,
1713 # so skip this too.
1714 messages.pop()
1715 else:
1716 # This is probably a CQ messages warranting user attention.
1717 break
1718
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001719 if not messages:
1720 # No message was sent.
1721 return 'unsent'
1722 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001723 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001724 return 'reply'
1725 return 'waiting'
1726
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001727 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001728 return self.RpcServer().update_description(
1729 self.GetIssue(), self.description)
1730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001732 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001733
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001734 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001735 return self.SetFlags({flag: value})
1736
1737 def SetFlags(self, flags):
1738 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001739 """
phajdan.jr68598232016-08-10 03:28:28 -07001740 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001741 try:
tandrii4b233bd2016-07-06 03:50:29 -07001742 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07001743 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07001744 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001745 if e.code == 404:
1746 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1747 if e.code == 403:
1748 DieWithError(
1749 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07001750 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001751 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001752
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001753 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754 """Returns an upload.RpcServer() to access this review's rietveld instance.
1755 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001756 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001757 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001759 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001760 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001761
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001762 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001763 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001764 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767 """Return the git setting that stores this change's most recent patchset."""
1768 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1769
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001770 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001772 branch = self.GetBranch()
1773 if branch:
1774 return 'branch.%s.rietveldserver' % branch
1775 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001776
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001777 def _PostUnsetIssueProperties(self):
1778 """Which branch-specific properties to erase when unsetting issue."""
1779 return ['rietveldserver']
1780
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001781 def GetRieveldObjForPresubmit(self):
1782 return self.RpcServer()
1783
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001784 def SetCQState(self, new_state):
1785 props = self.GetIssueProperties()
1786 if props.get('private'):
1787 DieWithError('Cannot set-commit on private issue')
1788
1789 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001790 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001791 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001792 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001793 else:
tandrii4b233bd2016-07-06 03:50:29 -07001794 assert new_state == _CQState.DRY_RUN
1795 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001796
1797
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001798 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1799 directory):
1800 # TODO(maruel): Use apply_issue.py
1801
1802 # PatchIssue should never be called with a dirty tree. It is up to the
1803 # caller to check this, but just in case we assert here since the
1804 # consequences of the caller not checking this could be dire.
1805 assert(not git_common.is_dirty_git_tree('apply'))
1806 assert(parsed_issue_arg.valid)
1807 self._changelist.issue = parsed_issue_arg.issue
1808 if parsed_issue_arg.hostname:
1809 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1810
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001811 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1812 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001813 assert parsed_issue_arg.patchset
1814 patchset = parsed_issue_arg.patchset
1815 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1816 else:
1817 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1818 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1819
1820 # Switch up to the top-level directory, if necessary, in preparation for
1821 # applying the patch.
1822 top = settings.GetRelativeRoot()
1823 if top:
1824 os.chdir(top)
1825
1826 # Git patches have a/ at the beginning of source paths. We strip that out
1827 # with a sed script rather than the -p flag to patch so we can feed either
1828 # Git or svn-style patches into the same apply command.
1829 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1830 try:
1831 patch_data = subprocess2.check_output(
1832 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1833 except subprocess2.CalledProcessError:
1834 DieWithError('Git patch mungling failed.')
1835 logging.info(patch_data)
1836
1837 # We use "git apply" to apply the patch instead of "patch" so that we can
1838 # pick up file adds.
1839 # The --index flag means: also insert into the index (so we catch adds).
1840 cmd = ['git', 'apply', '--index', '-p0']
1841 if directory:
1842 cmd.extend(('--directory', directory))
1843 if reject:
1844 cmd.append('--reject')
1845 elif IsGitVersionAtLeast('1.7.12'):
1846 cmd.append('--3way')
1847 try:
1848 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1849 stdin=patch_data, stdout=subprocess2.VOID)
1850 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001851 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 return 1
1853
1854 # If we had an issue, commit the current state and register the issue.
1855 if not nocommit:
1856 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1857 'patch from issue %(i)s at patchset '
1858 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1859 % {'i': self.GetIssue(), 'p': patchset})])
1860 self.SetIssue(self.GetIssue())
1861 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001862 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001863 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001864 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001865 return 0
1866
1867 @staticmethod
1868 def ParseIssueURL(parsed_url):
1869 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1870 return None
wychen3c1c1722016-08-04 11:46:36 -07001871 # Rietveld patch: https://domain/<number>/#ps<patchset>
1872 match = re.match(r'/(\d+)/$', parsed_url.path)
1873 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1874 if match and match2:
1875 return _RietveldParsedIssueNumberArgument(
1876 issue=int(match.group(1)),
1877 patchset=int(match2.group(1)),
1878 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001879 # Typical url: https://domain/<issue_number>[/[other]]
1880 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1881 if match:
1882 return _RietveldParsedIssueNumberArgument(
1883 issue=int(match.group(1)),
1884 hostname=parsed_url.netloc)
1885 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1886 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1887 if match:
1888 return _RietveldParsedIssueNumberArgument(
1889 issue=int(match.group(1)),
1890 patchset=int(match.group(2)),
1891 hostname=parsed_url.netloc,
1892 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1893 return None
1894
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001895 def CMDUploadChange(self, options, args, change):
1896 """Upload the patch to Rietveld."""
1897 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1898 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001899 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1900 if options.emulate_svn_auto_props:
1901 upload_args.append('--emulate_svn_auto_props')
1902
1903 change_desc = None
1904
1905 if options.email is not None:
1906 upload_args.extend(['--email', options.email])
1907
1908 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001909 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001910 upload_args.extend(['--title', options.title])
1911 if options.message:
1912 upload_args.extend(['--message', options.message])
1913 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001914 print('This branch is associated with issue %s. '
1915 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001916 else:
nodirca166002016-06-27 10:59:51 -07001917 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001918 upload_args.extend(['--title', options.title])
1919 message = (options.title or options.message or
1920 CreateDescriptionFromLog(args))
1921 change_desc = ChangeDescription(message)
1922 if options.reviewers or options.tbr_owners:
1923 change_desc.update_reviewers(options.reviewers,
1924 options.tbr_owners,
1925 change)
1926 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001927 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001928
1929 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001930 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001931 return 1
1932
1933 upload_args.extend(['--message', change_desc.description])
1934 if change_desc.get_reviewers():
1935 upload_args.append('--reviewers=%s' % ','.join(
1936 change_desc.get_reviewers()))
1937 if options.send_mail:
1938 if not change_desc.get_reviewers():
1939 DieWithError("Must specify reviewers to send email.")
1940 upload_args.append('--send_mail')
1941
1942 # We check this before applying rietveld.private assuming that in
1943 # rietveld.cc only addresses which we can send private CLs to are listed
1944 # if rietveld.private is set, and so we should ignore rietveld.cc only
1945 # when --private is specified explicitly on the command line.
1946 if options.private:
1947 logging.warn('rietveld.cc is ignored since private flag is specified. '
1948 'You need to review and add them manually if necessary.')
1949 cc = self.GetCCListWithoutDefault()
1950 else:
1951 cc = self.GetCCList()
1952 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1953 if cc:
1954 upload_args.extend(['--cc', cc])
1955
1956 if options.private or settings.GetDefaultPrivateFlag() == "True":
1957 upload_args.append('--private')
1958
1959 upload_args.extend(['--git_similarity', str(options.similarity)])
1960 if not options.find_copies:
1961 upload_args.extend(['--git_no_find_copies'])
1962
1963 # Include the upstream repo's URL in the change -- this is useful for
1964 # projects that have their source spread across multiple repos.
1965 remote_url = self.GetGitBaseUrlFromConfig()
1966 if not remote_url:
1967 if settings.GetIsGitSvn():
1968 remote_url = self.GetGitSvnRemoteUrl()
1969 else:
1970 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1971 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1972 self.GetUpstreamBranch().split('/')[-1])
1973 if remote_url:
1974 upload_args.extend(['--base_url', remote_url])
1975 remote, remote_branch = self.GetRemoteBranch()
1976 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1977 settings.GetPendingRefPrefix())
1978 if target_ref:
1979 upload_args.extend(['--target_ref', target_ref])
1980
1981 # Look for dependent patchsets. See crbug.com/480453 for more details.
1982 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1983 upstream_branch = ShortBranchName(upstream_branch)
1984 if remote is '.':
1985 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001986 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001987 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001988 print()
1989 print('Skipping dependency patchset upload because git config '
1990 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1991 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001992 else:
1993 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001994 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001995 auth_config=auth_config)
1996 branch_cl_issue_url = branch_cl.GetIssueURL()
1997 branch_cl_issue = branch_cl.GetIssue()
1998 branch_cl_patchset = branch_cl.GetPatchset()
1999 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2000 upload_args.extend(
2001 ['--depends_on_patchset', '%s:%s' % (
2002 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002003 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002004 '\n'
2005 'The current branch (%s) is tracking a local branch (%s) with '
2006 'an associated CL.\n'
2007 'Adding %s/#ps%s as a dependency patchset.\n'
2008 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2009 branch_cl_patchset))
2010
2011 project = settings.GetProject()
2012 if project:
2013 upload_args.extend(['--project', project])
2014
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002015 try:
2016 upload_args = ['upload'] + upload_args + args
2017 logging.info('upload.RealMain(%s)', upload_args)
2018 issue, patchset = upload.RealMain(upload_args)
2019 issue = int(issue)
2020 patchset = int(patchset)
2021 except KeyboardInterrupt:
2022 sys.exit(1)
2023 except:
2024 # If we got an exception after the user typed a description for their
2025 # change, back up the description before re-raising.
2026 if change_desc:
2027 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2028 print('\nGot exception while uploading -- saving description to %s\n' %
2029 backup_path)
2030 backup_file = open(backup_path, 'w')
2031 backup_file.write(change_desc.description)
2032 backup_file.close()
2033 raise
2034
2035 if not self.GetIssue():
2036 self.SetIssue(issue)
2037 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002038 return 0
2039
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002040
2041class _GerritChangelistImpl(_ChangelistCodereviewBase):
2042 def __init__(self, changelist, auth_config=None):
2043 # auth_config is Rietveld thing, kept here to preserve interface only.
2044 super(_GerritChangelistImpl, self).__init__(changelist)
2045 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002046 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002047 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002048 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002049
2050 def _GetGerritHost(self):
2051 # Lazy load of configs.
2052 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002053 if self._gerrit_host and '.' not in self._gerrit_host:
2054 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2055 # This happens for internal stuff http://crbug.com/614312.
2056 parsed = urlparse.urlparse(self.GetRemoteUrl())
2057 if parsed.scheme == 'sso':
2058 print('WARNING: using non https URLs for remote is likely broken\n'
2059 ' Your current remote is: %s' % self.GetRemoteUrl())
2060 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2061 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002062 return self._gerrit_host
2063
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002064 def _GetGitHost(self):
2065 """Returns git host to be used when uploading change to Gerrit."""
2066 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2067
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002068 def GetCodereviewServer(self):
2069 if not self._gerrit_server:
2070 # If we're on a branch then get the server potentially associated
2071 # with that branch.
2072 if self.GetIssue():
2073 gerrit_server_setting = self.GetCodereviewServerSetting()
2074 if gerrit_server_setting:
2075 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2076 error_ok=True).strip()
2077 if self._gerrit_server:
2078 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2079 if not self._gerrit_server:
2080 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2081 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002082 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002083 parts[0] = parts[0] + '-review'
2084 self._gerrit_host = '.'.join(parts)
2085 self._gerrit_server = 'https://%s' % self._gerrit_host
2086 return self._gerrit_server
2087
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002088 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002089 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002090 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002091
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002092 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002093 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002094 if settings.GetGerritSkipEnsureAuthenticated():
2095 # For projects with unusual authentication schemes.
2096 # See http://crbug.com/603378.
2097 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002098 # Lazy-loader to identify Gerrit and Git hosts.
2099 if gerrit_util.GceAuthenticator.is_gce():
2100 return
2101 self.GetCodereviewServer()
2102 git_host = self._GetGitHost()
2103 assert self._gerrit_server and self._gerrit_host
2104 cookie_auth = gerrit_util.CookiesAuthenticator()
2105
2106 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2107 git_auth = cookie_auth.get_auth_header(git_host)
2108 if gerrit_auth and git_auth:
2109 if gerrit_auth == git_auth:
2110 return
2111 print((
2112 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2113 ' Check your %s or %s file for credentials of hosts:\n'
2114 ' %s\n'
2115 ' %s\n'
2116 ' %s') %
2117 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2118 git_host, self._gerrit_host,
2119 cookie_auth.get_new_password_message(git_host)))
2120 if not force:
2121 ask_for_data('If you know what you are doing, press Enter to continue, '
2122 'Ctrl+C to abort.')
2123 return
2124 else:
2125 missing = (
2126 [] if gerrit_auth else [self._gerrit_host] +
2127 [] if git_auth else [git_host])
2128 DieWithError('Credentials for the following hosts are required:\n'
2129 ' %s\n'
2130 'These are read from %s (or legacy %s)\n'
2131 '%s' % (
2132 '\n '.join(missing),
2133 cookie_auth.get_gitcookies_path(),
2134 cookie_auth.get_netrc_path(),
2135 cookie_auth.get_new_password_message(git_host)))
2136
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002137
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002138 def PatchsetSetting(self):
2139 """Return the git setting that stores this change's most recent patchset."""
2140 return 'branch.%s.gerritpatchset' % self.GetBranch()
2141
2142 def GetCodereviewServerSetting(self):
2143 """Returns the git setting that stores this change's Gerrit server."""
2144 branch = self.GetBranch()
2145 if branch:
2146 return 'branch.%s.gerritserver' % branch
2147 return None
2148
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002149 def _PostUnsetIssueProperties(self):
2150 """Which branch-specific properties to erase when unsetting issue."""
2151 return [
2152 'gerritserver',
2153 'gerritsquashhash',
2154 ]
2155
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002156 def GetRieveldObjForPresubmit(self):
2157 class ThisIsNotRietveldIssue(object):
2158 def __nonzero__(self):
2159 # This is a hack to make presubmit_support think that rietveld is not
2160 # defined, yet still ensure that calls directly result in a decent
2161 # exception message below.
2162 return False
2163
2164 def __getattr__(self, attr):
2165 print(
2166 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2167 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2168 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2169 'or use Rietveld for codereview.\n'
2170 'See also http://crbug.com/579160.' % attr)
2171 raise NotImplementedError()
2172 return ThisIsNotRietveldIssue()
2173
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002174 def GetGerritObjForPresubmit(self):
2175 return presubmit_support.GerritAccessor(self._GetGerritHost())
2176
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002177 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002178 """Apply a rough heuristic to give a simple summary of an issue's review
2179 or CQ status, assuming adherence to a common workflow.
2180
2181 Returns None if no issue for this branch, or one of the following keywords:
2182 * 'error' - error from review tool (including deleted issues)
2183 * 'unsent' - no reviewers added
2184 * 'waiting' - waiting for review
2185 * 'reply' - waiting for owner to reply to review
2186 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2187 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2188 * 'commit' - in the commit queue
2189 * 'closed' - abandoned
2190 """
2191 if not self.GetIssue():
2192 return None
2193
2194 try:
2195 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2196 except httplib.HTTPException:
2197 return 'error'
2198
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002199 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002200 return 'closed'
2201
2202 cq_label = data['labels'].get('Commit-Queue', {})
2203 if cq_label:
2204 # Vote value is a stringified integer, which we expect from 0 to 2.
2205 vote_value = cq_label.get('value', '0')
2206 vote_text = cq_label.get('values', {}).get(vote_value, '')
2207 if vote_text.lower() == 'commit':
2208 return 'commit'
2209
2210 lgtm_label = data['labels'].get('Code-Review', {})
2211 if lgtm_label:
2212 if 'rejected' in lgtm_label:
2213 return 'not lgtm'
2214 if 'approved' in lgtm_label:
2215 return 'lgtm'
2216
2217 if not data.get('reviewers', {}).get('REVIEWER', []):
2218 return 'unsent'
2219
2220 messages = data.get('messages', [])
2221 if messages:
2222 owner = data['owner'].get('_account_id')
2223 last_message_author = messages[-1].get('author', {}).get('_account_id')
2224 if owner != last_message_author:
2225 # Some reply from non-owner.
2226 return 'reply'
2227
2228 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002229
2230 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002231 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002232 return data['revisions'][data['current_revision']]['_number']
2233
2234 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002235 data = self._GetChangeDetail(['CURRENT_REVISION'])
2236 current_rev = data['current_revision']
2237 url = data['revisions'][current_rev]['fetch']['http']['url']
2238 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002239
2240 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002241 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2242 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002243
2244 def CloseIssue(self):
2245 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2246
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002247 def GetApprovingReviewers(self):
2248 """Returns a list of reviewers approving the change.
2249
2250 Note: not necessarily committers.
2251 """
2252 raise NotImplementedError()
2253
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002254 def SubmitIssue(self, wait_for_merge=True):
2255 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2256 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002257
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002258 def _GetChangeDetail(self, options=None, issue=None):
2259 options = options or []
2260 issue = issue or self.GetIssue()
2261 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002262 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2263 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002264
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002265 def CMDLand(self, force, bypass_hooks, verbose):
2266 if git_common.is_dirty_git_tree('land'):
2267 return 1
tandriid60367b2016-06-22 05:25:12 -07002268 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2269 if u'Commit-Queue' in detail.get('labels', {}):
2270 if not force:
2271 ask_for_data('\nIt seems this repository has a Commit Queue, '
2272 'which can test and land changes for you. '
2273 'Are you sure you wish to bypass it?\n'
2274 'Press Enter to continue, Ctrl+C to abort.')
2275
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002276 differs = True
2277 last_upload = RunGit(['config',
2278 'branch.%s.gerritsquashhash' % self.GetBranch()],
2279 error_ok=True).strip()
2280 # Note: git diff outputs nothing if there is no diff.
2281 if not last_upload or RunGit(['diff', last_upload]).strip():
2282 print('WARNING: some changes from local branch haven\'t been uploaded')
2283 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002284 if detail['current_revision'] == last_upload:
2285 differs = False
2286 else:
2287 print('WARNING: local branch contents differ from latest uploaded '
2288 'patchset')
2289 if differs:
2290 if not force:
2291 ask_for_data(
2292 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2293 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2294 elif not bypass_hooks:
2295 hook_results = self.RunHook(
2296 committing=True,
2297 may_prompt=not force,
2298 verbose=verbose,
2299 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2300 if not hook_results.should_continue():
2301 return 1
2302
2303 self.SubmitIssue(wait_for_merge=True)
2304 print('Issue %s has been submitted.' % self.GetIssueURL())
2305 return 0
2306
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002307 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2308 directory):
2309 assert not reject
2310 assert not nocommit
2311 assert not directory
2312 assert parsed_issue_arg.valid
2313
2314 self._changelist.issue = parsed_issue_arg.issue
2315
2316 if parsed_issue_arg.hostname:
2317 self._gerrit_host = parsed_issue_arg.hostname
2318 self._gerrit_server = 'https://%s' % self._gerrit_host
2319
2320 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2321
2322 if not parsed_issue_arg.patchset:
2323 # Use current revision by default.
2324 revision_info = detail['revisions'][detail['current_revision']]
2325 patchset = int(revision_info['_number'])
2326 else:
2327 patchset = parsed_issue_arg.patchset
2328 for revision_info in detail['revisions'].itervalues():
2329 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2330 break
2331 else:
2332 DieWithError('Couldn\'t find patchset %i in issue %i' %
2333 (parsed_issue_arg.patchset, self.GetIssue()))
2334
2335 fetch_info = revision_info['fetch']['http']
2336 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2337 RunGit(['cherry-pick', 'FETCH_HEAD'])
2338 self.SetIssue(self.GetIssue())
2339 self.SetPatchset(patchset)
2340 print('Committed patch for issue %i pathset %i locally' %
2341 (self.GetIssue(), self.GetPatchset()))
2342 return 0
2343
2344 @staticmethod
2345 def ParseIssueURL(parsed_url):
2346 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2347 return None
2348 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2349 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2350 # Short urls like https://domain/<issue_number> can be used, but don't allow
2351 # specifying the patchset (you'd 404), but we allow that here.
2352 if parsed_url.path == '/':
2353 part = parsed_url.fragment
2354 else:
2355 part = parsed_url.path
2356 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2357 if match:
2358 return _ParsedIssueNumberArgument(
2359 issue=int(match.group(2)),
2360 patchset=int(match.group(4)) if match.group(4) else None,
2361 hostname=parsed_url.netloc)
2362 return None
2363
tandrii16e0b4e2016-06-07 10:34:28 -07002364 def _GerritCommitMsgHookCheck(self, offer_removal):
2365 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2366 if not os.path.exists(hook):
2367 return
2368 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2369 # custom developer made one.
2370 data = gclient_utils.FileRead(hook)
2371 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2372 return
2373 print('Warning: you have Gerrit commit-msg hook installed.\n'
2374 'It is not neccessary for uploading with git cl in squash mode, '
2375 'and may interfere with it in subtle ways.\n'
2376 'We recommend you remove the commit-msg hook.')
2377 if offer_removal:
2378 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2379 if reply.lower().startswith('y'):
2380 gclient_utils.rm_file_or_tree(hook)
2381 print('Gerrit commit-msg hook removed.')
2382 else:
2383 print('OK, will keep Gerrit commit-msg hook in place.')
2384
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002385 def CMDUploadChange(self, options, args, change):
2386 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002387 if options.squash and options.no_squash:
2388 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002389
2390 if not options.squash and not options.no_squash:
2391 # Load default for user, repo, squash=true, in this order.
2392 options.squash = settings.GetSquashGerritUploads()
2393 elif options.no_squash:
2394 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002395
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002396 # We assume the remote called "origin" is the one we want.
2397 # It is probably not worthwhile to support different workflows.
2398 gerrit_remote = 'origin'
2399
2400 remote, remote_branch = self.GetRemoteBranch()
2401 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2402 pending_prefix='')
2403
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002404 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002405 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002406 if self.GetIssue():
2407 # Try to get the message from a previous upload.
2408 message = self.GetDescription()
2409 if not message:
2410 DieWithError(
2411 'failed to fetch description from current Gerrit issue %d\n'
2412 '%s' % (self.GetIssue(), self.GetIssueURL()))
2413 change_id = self._GetChangeDetail()['change_id']
2414 while True:
2415 footer_change_ids = git_footers.get_footer_change_id(message)
2416 if footer_change_ids == [change_id]:
2417 break
2418 if not footer_change_ids:
2419 message = git_footers.add_footer_change_id(message, change_id)
2420 print('WARNING: appended missing Change-Id to issue description')
2421 continue
2422 # There is already a valid footer but with different or several ids.
2423 # Doing this automatically is non-trivial as we don't want to lose
2424 # existing other footers, yet we want to append just 1 desired
2425 # Change-Id. Thus, just create a new footer, but let user verify the
2426 # new description.
2427 message = '%s\n\nChange-Id: %s' % (message, change_id)
2428 print(
2429 'WARNING: issue %s has Change-Id footer(s):\n'
2430 ' %s\n'
2431 'but issue has Change-Id %s, according to Gerrit.\n'
2432 'Please, check the proposed correction to the description, '
2433 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2434 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2435 change_id))
2436 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2437 if not options.force:
2438 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002439 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002440 message = change_desc.description
2441 if not message:
2442 DieWithError("Description is empty. Aborting...")
2443 # Continue the while loop.
2444 # Sanity check of this code - we should end up with proper message
2445 # footer.
2446 assert [change_id] == git_footers.get_footer_change_id(message)
2447 change_desc = ChangeDescription(message)
2448 else:
2449 change_desc = ChangeDescription(
2450 options.message or CreateDescriptionFromLog(args))
2451 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002452 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002453 if not change_desc.description:
2454 DieWithError("Description is empty. Aborting...")
2455 message = change_desc.description
2456 change_ids = git_footers.get_footer_change_id(message)
2457 if len(change_ids) > 1:
2458 DieWithError('too many Change-Id footers, at most 1 allowed.')
2459 if not change_ids:
2460 # Generate the Change-Id automatically.
2461 message = git_footers.add_footer_change_id(
2462 message, GenerateGerritChangeId(message))
2463 change_desc.set_description(message)
2464 change_ids = git_footers.get_footer_change_id(message)
2465 assert len(change_ids) == 1
2466 change_id = change_ids[0]
2467
2468 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2469 if remote is '.':
2470 # If our upstream branch is local, we base our squashed commit on its
2471 # squashed version.
2472 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2473 # Check the squashed hash of the parent.
2474 parent = RunGit(['config',
2475 'branch.%s.gerritsquashhash' % upstream_branch_name],
2476 error_ok=True).strip()
2477 # Verify that the upstream branch has been uploaded too, otherwise
2478 # Gerrit will create additional CLs when uploading.
2479 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2480 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002481 DieWithError(
2482 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002483 'Note: maybe you\'ve uploaded it with --no-squash. '
2484 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002485 ' git cl upload --squash\n' % upstream_branch_name)
2486 else:
2487 parent = self.GetCommonAncestorWithUpstream()
2488
2489 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2490 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2491 '-m', message]).strip()
2492 else:
2493 change_desc = ChangeDescription(
2494 options.message or CreateDescriptionFromLog(args))
2495 if not change_desc.description:
2496 DieWithError("Description is empty. Aborting...")
2497
2498 if not git_footers.get_footer_change_id(change_desc.description):
2499 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002500 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2501 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002502 ref_to_push = 'HEAD'
2503 parent = '%s/%s' % (gerrit_remote, branch)
2504 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2505
2506 assert change_desc
2507 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2508 ref_to_push)]).splitlines()
2509 if len(commits) > 1:
2510 print('WARNING: This will upload %d commits. Run the following command '
2511 'to see which commits will be uploaded: ' % len(commits))
2512 print('git log %s..%s' % (parent, ref_to_push))
2513 print('You can also use `git squash-branch` to squash these into a '
2514 'single commit.')
2515 ask_for_data('About to upload; enter to confirm.')
2516
2517 if options.reviewers or options.tbr_owners:
2518 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2519 change)
2520
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002521 # Extra options that can be specified at push time. Doc:
2522 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2523 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002524 if change_desc.get_reviewers(tbr_only=True):
2525 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2526 refspec_opts.append('l=Code-Review+1')
2527
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002528 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002529 if not re.match(r'^[\w ]+$', options.title):
2530 options.title = re.sub(r'[^\w ]', '', options.title)
2531 print('WARNING: Patchset title may only contain alphanumeric chars '
2532 'and spaces. Cleaned up title:\n%s' % options.title)
2533 if not options.force:
2534 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002535 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2536 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002537 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2538
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002539 if options.send_mail:
2540 if not change_desc.get_reviewers():
2541 DieWithError('Must specify reviewers to send email.')
2542 refspec_opts.append('notify=ALL')
2543 else:
2544 refspec_opts.append('notify=NONE')
2545
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002546 cc = self.GetCCList().split(',')
2547 if options.cc:
2548 cc.extend(options.cc)
2549 cc = filter(None, cc)
2550 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002551 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002552
tandrii99a72f22016-08-17 14:33:24 -07002553 reviewers = change_desc.get_reviewers()
2554 if reviewers:
2555 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002556
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002557 refspec_suffix = ''
2558 if refspec_opts:
2559 refspec_suffix = '%' + ','.join(refspec_opts)
2560 assert ' ' not in refspec_suffix, (
2561 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002562 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002563
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002564 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002565 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002566 print_stdout=True,
2567 # Flush after every line: useful for seeing progress when running as
2568 # recipe.
2569 filter_fn=lambda _: sys.stdout.flush())
2570
2571 if options.squash:
2572 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2573 change_numbers = [m.group(1)
2574 for m in map(regex.match, push_stdout.splitlines())
2575 if m]
2576 if len(change_numbers) != 1:
2577 DieWithError(
2578 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2579 'Change-Id: %s') % (len(change_numbers), change_id))
2580 self.SetIssue(change_numbers[0])
2581 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2582 ref_to_push])
2583 return 0
2584
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002585 def _AddChangeIdToCommitMessage(self, options, args):
2586 """Re-commits using the current message, assumes the commit hook is in
2587 place.
2588 """
2589 log_desc = options.message or CreateDescriptionFromLog(args)
2590 git_command = ['commit', '--amend', '-m', log_desc]
2591 RunGit(git_command)
2592 new_log_desc = CreateDescriptionFromLog(args)
2593 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002594 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002595 return new_log_desc
2596 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002597 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002598
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002599 def SetCQState(self, new_state):
2600 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002601 vote_map = {
2602 _CQState.NONE: 0,
2603 _CQState.DRY_RUN: 1,
2604 _CQState.COMMIT : 2,
2605 }
2606 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2607 labels={'Commit-Queue': vote_map[new_state]})
2608
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002609
2610_CODEREVIEW_IMPLEMENTATIONS = {
2611 'rietveld': _RietveldChangelistImpl,
2612 'gerrit': _GerritChangelistImpl,
2613}
2614
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002615
iannuccie53c9352016-08-17 14:40:40 -07002616def _add_codereview_issue_select_options(parser, extra=""):
2617 _add_codereview_select_options(parser)
2618
2619 text = ('Operate on this issue number instead of the current branch\'s '
2620 'implicit issue.')
2621 if extra:
2622 text += ' '+extra
2623 parser.add_option('-i', '--issue', type=int, help=text)
2624
2625
2626def _process_codereview_issue_select_options(parser, options):
2627 _process_codereview_select_options(parser, options)
2628 if options.issue is not None and not options.forced_codereview:
2629 parser.error('--issue must be specified with either --rietveld or --gerrit')
2630
2631
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002632def _add_codereview_select_options(parser):
2633 """Appends --gerrit and --rietveld options to force specific codereview."""
2634 parser.codereview_group = optparse.OptionGroup(
2635 parser, 'EXPERIMENTAL! Codereview override options')
2636 parser.add_option_group(parser.codereview_group)
2637 parser.codereview_group.add_option(
2638 '--gerrit', action='store_true',
2639 help='Force the use of Gerrit for codereview')
2640 parser.codereview_group.add_option(
2641 '--rietveld', action='store_true',
2642 help='Force the use of Rietveld for codereview')
2643
2644
2645def _process_codereview_select_options(parser, options):
2646 if options.gerrit and options.rietveld:
2647 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2648 options.forced_codereview = None
2649 if options.gerrit:
2650 options.forced_codereview = 'gerrit'
2651 elif options.rietveld:
2652 options.forced_codereview = 'rietveld'
2653
2654
tandriif9aefb72016-07-01 09:06:51 -07002655def _get_bug_line_values(default_project, bugs):
2656 """Given default_project and comma separated list of bugs, yields bug line
2657 values.
2658
2659 Each bug can be either:
2660 * a number, which is combined with default_project
2661 * string, which is left as is.
2662
2663 This function may produce more than one line, because bugdroid expects one
2664 project per line.
2665
2666 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2667 ['v8:123', 'chromium:789']
2668 """
2669 default_bugs = []
2670 others = []
2671 for bug in bugs.split(','):
2672 bug = bug.strip()
2673 if bug:
2674 try:
2675 default_bugs.append(int(bug))
2676 except ValueError:
2677 others.append(bug)
2678
2679 if default_bugs:
2680 default_bugs = ','.join(map(str, default_bugs))
2681 if default_project:
2682 yield '%s:%s' % (default_project, default_bugs)
2683 else:
2684 yield default_bugs
2685 for other in sorted(others):
2686 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2687 yield other
2688
2689
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002690class ChangeDescription(object):
2691 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002692 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002693 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002694
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002695 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002696 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002697
agable@chromium.org42c20792013-09-12 17:34:49 +00002698 @property # www.logilab.org/ticket/89786
2699 def description(self): # pylint: disable=E0202
2700 return '\n'.join(self._description_lines)
2701
2702 def set_description(self, desc):
2703 if isinstance(desc, basestring):
2704 lines = desc.splitlines()
2705 else:
2706 lines = [line.rstrip() for line in desc]
2707 while lines and not lines[0]:
2708 lines.pop(0)
2709 while lines and not lines[-1]:
2710 lines.pop(-1)
2711 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002712
piman@chromium.org336f9122014-09-04 02:16:55 +00002713 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002714 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002715 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002716 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002717 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002718 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002719
agable@chromium.org42c20792013-09-12 17:34:49 +00002720 # Get the set of R= and TBR= lines and remove them from the desciption.
2721 regexp = re.compile(self.R_LINE)
2722 matches = [regexp.match(line) for line in self._description_lines]
2723 new_desc = [l for i, l in enumerate(self._description_lines)
2724 if not matches[i]]
2725 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002726
agable@chromium.org42c20792013-09-12 17:34:49 +00002727 # Construct new unified R= and TBR= lines.
2728 r_names = []
2729 tbr_names = []
2730 for match in matches:
2731 if not match:
2732 continue
2733 people = cleanup_list([match.group(2).strip()])
2734 if match.group(1) == 'TBR':
2735 tbr_names.extend(people)
2736 else:
2737 r_names.extend(people)
2738 for name in r_names:
2739 if name not in reviewers:
2740 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002741 if add_owners_tbr:
2742 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002743 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002744 all_reviewers = set(tbr_names + reviewers)
2745 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2746 all_reviewers)
2747 tbr_names.extend(owners_db.reviewers_for(missing_files,
2748 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002749 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2750 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2751
2752 # Put the new lines in the description where the old first R= line was.
2753 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2754 if 0 <= line_loc < len(self._description_lines):
2755 if new_tbr_line:
2756 self._description_lines.insert(line_loc, new_tbr_line)
2757 if new_r_line:
2758 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002759 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002760 if new_r_line:
2761 self.append_footer(new_r_line)
2762 if new_tbr_line:
2763 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002764
tandriif9aefb72016-07-01 09:06:51 -07002765 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002766 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002767 self.set_description([
2768 '# Enter a description of the change.',
2769 '# This will be displayed on the codereview site.',
2770 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002771 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002772 '--------------------',
2773 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002774
agable@chromium.org42c20792013-09-12 17:34:49 +00002775 regexp = re.compile(self.BUG_LINE)
2776 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002777 prefix = settings.GetBugPrefix()
2778 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2779 for value in values:
2780 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2781 self.append_footer('BUG=%s' % value)
2782
agable@chromium.org42c20792013-09-12 17:34:49 +00002783 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002784 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002785 if not content:
2786 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002787 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002788
2789 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002790 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2791 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002792 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002793 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002794
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002795 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002796 """Adds a footer line to the description.
2797
2798 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2799 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2800 that Gerrit footers are always at the end.
2801 """
2802 parsed_footer_line = git_footers.parse_footer(line)
2803 if parsed_footer_line:
2804 # Line is a gerrit footer in the form: Footer-Key: any value.
2805 # Thus, must be appended observing Gerrit footer rules.
2806 self.set_description(
2807 git_footers.add_footer(self.description,
2808 key=parsed_footer_line[0],
2809 value=parsed_footer_line[1]))
2810 return
2811
2812 if not self._description_lines:
2813 self._description_lines.append(line)
2814 return
2815
2816 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2817 if gerrit_footers:
2818 # git_footers.split_footers ensures that there is an empty line before
2819 # actual (gerrit) footers, if any. We have to keep it that way.
2820 assert top_lines and top_lines[-1] == ''
2821 top_lines, separator = top_lines[:-1], top_lines[-1:]
2822 else:
2823 separator = [] # No need for separator if there are no gerrit_footers.
2824
2825 prev_line = top_lines[-1] if top_lines else ''
2826 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2827 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2828 top_lines.append('')
2829 top_lines.append(line)
2830 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002831
tandrii99a72f22016-08-17 14:33:24 -07002832 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002833 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002834 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002835 reviewers = [match.group(2).strip()
2836 for match in matches
2837 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002838 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002839
2840
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002841def get_approving_reviewers(props):
2842 """Retrieves the reviewers that approved a CL from the issue properties with
2843 messages.
2844
2845 Note that the list may contain reviewers that are not committer, thus are not
2846 considered by the CQ.
2847 """
2848 return sorted(
2849 set(
2850 message['sender']
2851 for message in props['messages']
2852 if message['approval'] and message['sender'] in props['reviewers']
2853 )
2854 )
2855
2856
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002857def FindCodereviewSettingsFile(filename='codereview.settings'):
2858 """Finds the given file starting in the cwd and going up.
2859
2860 Only looks up to the top of the repository unless an
2861 'inherit-review-settings-ok' file exists in the root of the repository.
2862 """
2863 inherit_ok_file = 'inherit-review-settings-ok'
2864 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002865 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002866 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2867 root = '/'
2868 while True:
2869 if filename in os.listdir(cwd):
2870 if os.path.isfile(os.path.join(cwd, filename)):
2871 return open(os.path.join(cwd, filename))
2872 if cwd == root:
2873 break
2874 cwd = os.path.dirname(cwd)
2875
2876
2877def LoadCodereviewSettingsFromFile(fileobj):
2878 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002879 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002881 def SetProperty(name, setting, unset_error_ok=False):
2882 fullname = 'rietveld.' + name
2883 if setting in keyvals:
2884 RunGit(['config', fullname, keyvals[setting]])
2885 else:
2886 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2887
2888 SetProperty('server', 'CODE_REVIEW_SERVER')
2889 # Only server setting is required. Other settings can be absent.
2890 # In that case, we ignore errors raised during option deletion attempt.
2891 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002892 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002893 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2894 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002895 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002896 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002897 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2898 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002899 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002900 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002901 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002902 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2903 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002904
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002905 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002906 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002907
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002908 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002909 RunGit(['config', 'gerrit.squash-uploads',
2910 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002911
tandrii@chromium.org28253532016-04-14 13:46:56 +00002912 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002913 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002914 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2915
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002916 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2917 #should be of the form
2918 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2919 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2920 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2921 keyvals['ORIGIN_URL_CONFIG']])
2922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002923
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002924def urlretrieve(source, destination):
2925 """urllib is broken for SSL connections via a proxy therefore we
2926 can't use urllib.urlretrieve()."""
2927 with open(destination, 'w') as f:
2928 f.write(urllib2.urlopen(source).read())
2929
2930
ukai@chromium.org712d6102013-11-27 00:52:58 +00002931def hasSheBang(fname):
2932 """Checks fname is a #! script."""
2933 with open(fname) as f:
2934 return f.read(2).startswith('#!')
2935
2936
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002937# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2938def DownloadHooks(*args, **kwargs):
2939 pass
2940
2941
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002942def DownloadGerritHook(force):
2943 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002944
2945 Args:
2946 force: True to update hooks. False to install hooks if not present.
2947 """
2948 if not settings.GetIsGerrit():
2949 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002950 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002951 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2952 if not os.access(dst, os.X_OK):
2953 if os.path.exists(dst):
2954 if not force:
2955 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002956 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002957 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002958 if not hasSheBang(dst):
2959 DieWithError('Not a script: %s\n'
2960 'You need to download from\n%s\n'
2961 'into .git/hooks/commit-msg and '
2962 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002963 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2964 except Exception:
2965 if os.path.exists(dst):
2966 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002967 DieWithError('\nFailed to download hooks.\n'
2968 'You need to download from\n%s\n'
2969 'into .git/hooks/commit-msg and '
2970 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002971
2972
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002973
2974def GetRietveldCodereviewSettingsInteractively():
2975 """Prompt the user for settings."""
2976 server = settings.GetDefaultServerUrl(error_ok=True)
2977 prompt = 'Rietveld server (host[:port])'
2978 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2979 newserver = ask_for_data(prompt + ':')
2980 if not server and not newserver:
2981 newserver = DEFAULT_SERVER
2982 if newserver:
2983 newserver = gclient_utils.UpgradeToHttps(newserver)
2984 if newserver != server:
2985 RunGit(['config', 'rietveld.server', newserver])
2986
2987 def SetProperty(initial, caption, name, is_url):
2988 prompt = caption
2989 if initial:
2990 prompt += ' ("x" to clear) [%s]' % initial
2991 new_val = ask_for_data(prompt + ':')
2992 if new_val == 'x':
2993 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2994 elif new_val:
2995 if is_url:
2996 new_val = gclient_utils.UpgradeToHttps(new_val)
2997 if new_val != initial:
2998 RunGit(['config', 'rietveld.' + name, new_val])
2999
3000 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3001 SetProperty(settings.GetDefaultPrivateFlag(),
3002 'Private flag (rietveld only)', 'private', False)
3003 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3004 'tree-status-url', False)
3005 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3006 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3007 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3008 'run-post-upload-hook', False)
3009
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003010@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003011def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003012 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003013
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003014 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003015 'For Gerrit, see http://crbug.com/603116.')
3016 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003017 parser.add_option('--activate-update', action='store_true',
3018 help='activate auto-updating [rietveld] section in '
3019 '.git/config')
3020 parser.add_option('--deactivate-update', action='store_true',
3021 help='deactivate auto-updating [rietveld] section in '
3022 '.git/config')
3023 options, args = parser.parse_args(args)
3024
3025 if options.deactivate_update:
3026 RunGit(['config', 'rietveld.autoupdate', 'false'])
3027 return
3028
3029 if options.activate_update:
3030 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3031 return
3032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003033 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003034 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003035 return 0
3036
3037 url = args[0]
3038 if not url.endswith('codereview.settings'):
3039 url = os.path.join(url, 'codereview.settings')
3040
3041 # Load code review settings and download hooks (if available).
3042 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3043 return 0
3044
3045
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003046def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003047 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003048 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3049 branch = ShortBranchName(branchref)
3050 _, args = parser.parse_args(args)
3051 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003052 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003053 return RunGit(['config', 'branch.%s.base-url' % branch],
3054 error_ok=False).strip()
3055 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003056 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003057 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3058 error_ok=False).strip()
3059
3060
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003061def color_for_status(status):
3062 """Maps a Changelist status to color, for CMDstatus and other tools."""
3063 return {
3064 'unsent': Fore.RED,
3065 'waiting': Fore.BLUE,
3066 'reply': Fore.YELLOW,
3067 'lgtm': Fore.GREEN,
3068 'commit': Fore.MAGENTA,
3069 'closed': Fore.CYAN,
3070 'error': Fore.WHITE,
3071 }.get(status, Fore.WHITE)
3072
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003073
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003074def get_cl_statuses(changes, fine_grained, max_processes=None):
3075 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003076
3077 If fine_grained is true, this will fetch CL statuses from the server.
3078 Otherwise, simply indicate if there's a matching url for the given branches.
3079
3080 If max_processes is specified, it is used as the maximum number of processes
3081 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3082 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003083
3084 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003085 """
3086 # Silence upload.py otherwise it becomes unwieldly.
3087 upload.verbosity = 0
3088
3089 if fine_grained:
3090 # Process one branch synchronously to work through authentication, then
3091 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003092 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003093 def fetch(cl):
3094 try:
3095 return (cl, cl.GetStatus())
3096 except:
3097 # See http://crbug.com/629863.
3098 logging.exception('failed to fetch status for %s:', cl)
3099 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003100 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003101
tandriiea9514a2016-08-17 12:32:37 -07003102 changes_to_fetch = changes[1:]
3103 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003104 # Exit early if there was only one branch to fetch.
3105 return
3106
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003107 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003108 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003109 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003110 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003111
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003112 fetched_cls = set()
3113 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003114 while True:
3115 try:
3116 row = it.next(timeout=5)
3117 except multiprocessing.TimeoutError:
3118 break
3119
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003120 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003121 yield row
3122
3123 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003124 for cl in set(changes_to_fetch) - fetched_cls:
3125 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003126
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003127 else:
3128 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003129 for cl in changes:
3130 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003131
rmistry@google.com2dd99862015-06-22 12:22:18 +00003132
3133def upload_branch_deps(cl, args):
3134 """Uploads CLs of local branches that are dependents of the current branch.
3135
3136 If the local branch dependency tree looks like:
3137 test1 -> test2.1 -> test3.1
3138 -> test3.2
3139 -> test2.2 -> test3.3
3140
3141 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3142 run on the dependent branches in this order:
3143 test2.1, test3.1, test3.2, test2.2, test3.3
3144
3145 Note: This function does not rebase your local dependent branches. Use it when
3146 you make a change to the parent branch that will not conflict with its
3147 dependent branches, and you would like their dependencies updated in
3148 Rietveld.
3149 """
3150 if git_common.is_dirty_git_tree('upload-branch-deps'):
3151 return 1
3152
3153 root_branch = cl.GetBranch()
3154 if root_branch is None:
3155 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3156 'Get on a branch!')
3157 if not cl.GetIssue() or not cl.GetPatchset():
3158 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3159 'patchset dependencies without an uploaded CL.')
3160
3161 branches = RunGit(['for-each-ref',
3162 '--format=%(refname:short) %(upstream:short)',
3163 'refs/heads'])
3164 if not branches:
3165 print('No local branches found.')
3166 return 0
3167
3168 # Create a dictionary of all local branches to the branches that are dependent
3169 # on it.
3170 tracked_to_dependents = collections.defaultdict(list)
3171 for b in branches.splitlines():
3172 tokens = b.split()
3173 if len(tokens) == 2:
3174 branch_name, tracked = tokens
3175 tracked_to_dependents[tracked].append(branch_name)
3176
vapiera7fbd5a2016-06-16 09:17:49 -07003177 print()
3178 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003179 dependents = []
3180 def traverse_dependents_preorder(branch, padding=''):
3181 dependents_to_process = tracked_to_dependents.get(branch, [])
3182 padding += ' '
3183 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003184 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003185 dependents.append(dependent)
3186 traverse_dependents_preorder(dependent, padding)
3187 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003188 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003189
3190 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003191 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003192 return 0
3193
vapiera7fbd5a2016-06-16 09:17:49 -07003194 print('This command will checkout all dependent branches and run '
3195 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003196 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3197
andybons@chromium.org962f9462016-02-03 20:00:42 +00003198 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003199 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003200 args.extend(['-t', 'Updated patchset dependency'])
3201
rmistry@google.com2dd99862015-06-22 12:22:18 +00003202 # Record all dependents that failed to upload.
3203 failures = {}
3204 # Go through all dependents, checkout the branch and upload.
3205 try:
3206 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003207 print()
3208 print('--------------------------------------')
3209 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003210 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003211 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003212 try:
3213 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003214 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003215 failures[dependent_branch] = 1
3216 except: # pylint: disable=W0702
3217 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003218 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003219 finally:
3220 # Swap back to the original root branch.
3221 RunGit(['checkout', '-q', root_branch])
3222
vapiera7fbd5a2016-06-16 09:17:49 -07003223 print()
3224 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003225 for dependent_branch in dependents:
3226 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003227 print(' %s : %s' % (dependent_branch, upload_status))
3228 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003229
3230 return 0
3231
3232
kmarshall3bff56b2016-06-06 18:31:47 -07003233def CMDarchive(parser, args):
3234 """Archives and deletes branches associated with closed changelists."""
3235 parser.add_option(
3236 '-j', '--maxjobs', action='store', type=int,
3237 help='The maximum number of jobs to use when retrieving review status')
3238 parser.add_option(
3239 '-f', '--force', action='store_true',
3240 help='Bypasses the confirmation prompt.')
3241
3242 auth.add_auth_options(parser)
3243 options, args = parser.parse_args(args)
3244 if args:
3245 parser.error('Unsupported args: %s' % ' '.join(args))
3246 auth_config = auth.extract_auth_config_from_options(options)
3247
3248 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3249 if not branches:
3250 return 0
3251
vapiera7fbd5a2016-06-16 09:17:49 -07003252 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003253 changes = [Changelist(branchref=b, auth_config=auth_config)
3254 for b in branches.splitlines()]
3255 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3256 statuses = get_cl_statuses(changes,
3257 fine_grained=True,
3258 max_processes=options.maxjobs)
3259 proposal = [(cl.GetBranch(),
3260 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3261 for cl, status in statuses
3262 if status == 'closed']
3263 proposal.sort()
3264
3265 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003266 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003267 return 0
3268
3269 current_branch = GetCurrentBranch()
3270
vapiera7fbd5a2016-06-16 09:17:49 -07003271 print('\nBranches with closed issues that will be archived:\n')
3272 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003273 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003274 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003275
3276 if any(branch == current_branch for branch, _ in proposal):
3277 print('You are currently on a branch \'%s\' which is associated with a '
3278 'closed codereview issue, so archive cannot proceed. Please '
3279 'checkout another branch and run this command again.' %
3280 current_branch)
3281 return 1
3282
3283 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003284 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3285 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003286 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003287 return 1
3288
3289 for branch, tagname in proposal:
3290 RunGit(['tag', tagname, branch])
3291 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003292 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003293
3294 return 0
3295
3296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003297def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003298 """Show status of changelists.
3299
3300 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003301 - Red not sent for review or broken
3302 - Blue waiting for review
3303 - Yellow waiting for you to reply to review
3304 - Green LGTM'ed
3305 - Magenta in the commit queue
3306 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003307
3308 Also see 'git cl comments'.
3309 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003310 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003311 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003312 parser.add_option('-f', '--fast', action='store_true',
3313 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003314 parser.add_option(
3315 '-j', '--maxjobs', action='store', type=int,
3316 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003317
3318 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003319 _add_codereview_issue_select_options(
3320 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003321 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003322 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003323 if args:
3324 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003325 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003326
iannuccie53c9352016-08-17 14:40:40 -07003327 if options.issue is not None and not options.field:
3328 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003329
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003330 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003331 cl = Changelist(auth_config=auth_config, issue=options.issue,
3332 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003333 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003334 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003335 elif options.field == 'id':
3336 issueid = cl.GetIssue()
3337 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003338 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003339 elif options.field == 'patch':
3340 patchset = cl.GetPatchset()
3341 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003342 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003343 elif options.field == 'status':
3344 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003345 elif options.field == 'url':
3346 url = cl.GetIssueURL()
3347 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003348 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003349 return 0
3350
3351 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3352 if not branches:
3353 print('No local branch found.')
3354 return 0
3355
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003356 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003357 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003358 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003359 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003360 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003361 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003362 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003363
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003364 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003365 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3366 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3367 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003368 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003369 c, status = output.next()
3370 branch_statuses[c.GetBranch()] = status
3371 status = branch_statuses.pop(branch)
3372 url = cl.GetIssueURL()
3373 if url and (not status or status == 'error'):
3374 # The issue probably doesn't exist anymore.
3375 url += ' (broken)'
3376
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003377 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003378 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003379 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003380 color = ''
3381 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003382 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003383 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003384 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003385 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003386
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003387 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003388 print()
3389 print('Current branch:',)
3390 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003391 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003392 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003393 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003394 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003395 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003396 print('Issue description:')
3397 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398 return 0
3399
3400
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003401def colorize_CMDstatus_doc():
3402 """To be called once in main() to add colors to git cl status help."""
3403 colors = [i for i in dir(Fore) if i[0].isupper()]
3404
3405 def colorize_line(line):
3406 for color in colors:
3407 if color in line.upper():
3408 # Extract whitespaces first and the leading '-'.
3409 indent = len(line) - len(line.lstrip(' ')) + 1
3410 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3411 return line
3412
3413 lines = CMDstatus.__doc__.splitlines()
3414 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3415
3416
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003417@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003418def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003419 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003420
3421 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003422 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003423 parser.add_option('-r', '--reverse', action='store_true',
3424 help='Lookup the branch(es) for the specified issues. If '
3425 'no issues are specified, all branches with mapped '
3426 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003427 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003428 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003429 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003430
dnj@chromium.org406c4402015-03-03 17:22:28 +00003431 if options.reverse:
3432 branches = RunGit(['for-each-ref', 'refs/heads',
3433 '--format=%(refname:short)']).splitlines()
3434
3435 # Reverse issue lookup.
3436 issue_branch_map = {}
3437 for branch in branches:
3438 cl = Changelist(branchref=branch)
3439 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3440 if not args:
3441 args = sorted(issue_branch_map.iterkeys())
3442 for issue in args:
3443 if not issue:
3444 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003445 print('Branch for issue number %s: %s' % (
3446 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003447 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003448 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003449 if len(args) > 0:
3450 try:
3451 issue = int(args[0])
3452 except ValueError:
3453 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003454 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003455 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003456 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003457 return 0
3458
3459
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003460def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003461 """Shows or posts review comments for any changelist."""
3462 parser.add_option('-a', '--add-comment', dest='comment',
3463 help='comment to add to an issue')
3464 parser.add_option('-i', dest='issue',
3465 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003466 parser.add_option('-j', '--json-file',
3467 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003468 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003469 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003470 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003471
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003472 issue = None
3473 if options.issue:
3474 try:
3475 issue = int(options.issue)
3476 except ValueError:
3477 DieWithError('A review issue id is expected to be a number')
3478
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003479 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003480
3481 if options.comment:
3482 cl.AddComment(options.comment)
3483 return 0
3484
3485 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003486 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003487 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003488 summary.append({
3489 'date': message['date'],
3490 'lgtm': False,
3491 'message': message['text'],
3492 'not_lgtm': False,
3493 'sender': message['sender'],
3494 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003495 if message['disapproval']:
3496 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003497 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003498 elif message['approval']:
3499 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003500 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003501 elif message['sender'] == data['owner_email']:
3502 color = Fore.MAGENTA
3503 else:
3504 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003505 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003506 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003507 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003508 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003509 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003510 if options.json_file:
3511 with open(options.json_file, 'wb') as f:
3512 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003513 return 0
3514
3515
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003516@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003517def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003518 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003519 parser.add_option('-d', '--display', action='store_true',
3520 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003521 parser.add_option('-n', '--new-description',
3522 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003523
3524 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003525 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003526 options, args = parser.parse_args(args)
3527 _process_codereview_select_options(parser, options)
3528
3529 target_issue = None
3530 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003531 target_issue = ParseIssueNumberArgument(args[0])
3532 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003533 parser.print_help()
3534 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003535
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003536 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003537
martiniss6eda05f2016-06-30 10:18:35 -07003538 kwargs = {
3539 'auth_config': auth_config,
3540 'codereview': options.forced_codereview,
3541 }
3542 if target_issue:
3543 kwargs['issue'] = target_issue.issue
3544 if options.forced_codereview == 'rietveld':
3545 kwargs['rietveld_server'] = target_issue.hostname
3546
3547 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003548
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003549 if not cl.GetIssue():
3550 DieWithError('This branch has no associated changelist.')
3551 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003552
smut@google.com34fb6b12015-07-13 20:03:26 +00003553 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003554 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003555 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003556
3557 if options.new_description:
3558 text = options.new_description
3559 if text == '-':
3560 text = '\n'.join(l.rstrip() for l in sys.stdin)
3561
3562 description.set_description(text)
3563 else:
3564 description.prompt()
3565
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003566 if cl.GetDescription() != description.description:
3567 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003568 return 0
3569
3570
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003571def CreateDescriptionFromLog(args):
3572 """Pulls out the commit log to use as a base for the CL description."""
3573 log_args = []
3574 if len(args) == 1 and not args[0].endswith('.'):
3575 log_args = [args[0] + '..']
3576 elif len(args) == 1 and args[0].endswith('...'):
3577 log_args = [args[0][:-1]]
3578 elif len(args) == 2:
3579 log_args = [args[0] + '..' + args[1]]
3580 else:
3581 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003582 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583
3584
thestig@chromium.org44202a22014-03-11 19:22:18 +00003585def CMDlint(parser, args):
3586 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003587 parser.add_option('--filter', action='append', metavar='-x,+y',
3588 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003589 auth.add_auth_options(parser)
3590 options, args = parser.parse_args(args)
3591 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003592
3593 # Access to a protected member _XX of a client class
3594 # pylint: disable=W0212
3595 try:
3596 import cpplint
3597 import cpplint_chromium
3598 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003599 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003600 return 1
3601
3602 # Change the current working directory before calling lint so that it
3603 # shows the correct base.
3604 previous_cwd = os.getcwd()
3605 os.chdir(settings.GetRoot())
3606 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003607 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003608 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3609 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003610 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003611 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003612 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003613
3614 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003615 command = args + files
3616 if options.filter:
3617 command = ['--filter=' + ','.join(options.filter)] + command
3618 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003619
3620 white_regex = re.compile(settings.GetLintRegex())
3621 black_regex = re.compile(settings.GetLintIgnoreRegex())
3622 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3623 for filename in filenames:
3624 if white_regex.match(filename):
3625 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003626 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003627 else:
3628 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3629 extra_check_functions)
3630 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003631 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003632 finally:
3633 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003634 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003635 if cpplint._cpplint_state.error_count != 0:
3636 return 1
3637 return 0
3638
3639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003640def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003641 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003642 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003643 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003644 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003645 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003646 auth.add_auth_options(parser)
3647 options, args = parser.parse_args(args)
3648 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003649
sbc@chromium.org71437c02015-04-09 19:29:40 +00003650 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003651 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003652 return 1
3653
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003654 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003655 if args:
3656 base_branch = args[0]
3657 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003658 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003659 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003660
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003661 cl.RunHook(
3662 committing=not options.upload,
3663 may_prompt=False,
3664 verbose=options.verbose,
3665 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003666 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003667
3668
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003669def GenerateGerritChangeId(message):
3670 """Returns Ixxxxxx...xxx change id.
3671
3672 Works the same way as
3673 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3674 but can be called on demand on all platforms.
3675
3676 The basic idea is to generate git hash of a state of the tree, original commit
3677 message, author/committer info and timestamps.
3678 """
3679 lines = []
3680 tree_hash = RunGitSilent(['write-tree'])
3681 lines.append('tree %s' % tree_hash.strip())
3682 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3683 if code == 0:
3684 lines.append('parent %s' % parent.strip())
3685 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3686 lines.append('author %s' % author.strip())
3687 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3688 lines.append('committer %s' % committer.strip())
3689 lines.append('')
3690 # Note: Gerrit's commit-hook actually cleans message of some lines and
3691 # whitespace. This code is not doing this, but it clearly won't decrease
3692 # entropy.
3693 lines.append(message)
3694 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3695 stdin='\n'.join(lines))
3696 return 'I%s' % change_hash.strip()
3697
3698
wittman@chromium.org455dc922015-01-26 20:15:50 +00003699def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3700 """Computes the remote branch ref to use for the CL.
3701
3702 Args:
3703 remote (str): The git remote for the CL.
3704 remote_branch (str): The git remote branch for the CL.
3705 target_branch (str): The target branch specified by the user.
3706 pending_prefix (str): The pending prefix from the settings.
3707 """
3708 if not (remote and remote_branch):
3709 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003710
wittman@chromium.org455dc922015-01-26 20:15:50 +00003711 if target_branch:
3712 # Cannonicalize branch references to the equivalent local full symbolic
3713 # refs, which are then translated into the remote full symbolic refs
3714 # below.
3715 if '/' not in target_branch:
3716 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3717 else:
3718 prefix_replacements = (
3719 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3720 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3721 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3722 )
3723 match = None
3724 for regex, replacement in prefix_replacements:
3725 match = re.search(regex, target_branch)
3726 if match:
3727 remote_branch = target_branch.replace(match.group(0), replacement)
3728 break
3729 if not match:
3730 # This is a branch path but not one we recognize; use as-is.
3731 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003732 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3733 # Handle the refs that need to land in different refs.
3734 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003735
wittman@chromium.org455dc922015-01-26 20:15:50 +00003736 # Create the true path to the remote branch.
3737 # Does the following translation:
3738 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3739 # * refs/remotes/origin/master -> refs/heads/master
3740 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3741 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3742 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3743 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3744 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3745 'refs/heads/')
3746 elif remote_branch.startswith('refs/remotes/branch-heads'):
3747 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3748 # If a pending prefix exists then replace refs/ with it.
3749 if pending_prefix:
3750 remote_branch = remote_branch.replace('refs/', pending_prefix)
3751 return remote_branch
3752
3753
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003754def cleanup_list(l):
3755 """Fixes a list so that comma separated items are put as individual items.
3756
3757 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3758 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3759 """
3760 items = sum((i.split(',') for i in l), [])
3761 stripped_items = (i.strip() for i in items)
3762 return sorted(filter(None, stripped_items))
3763
3764
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003765@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003766def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003767 """Uploads the current changelist to codereview.
3768
3769 Can skip dependency patchset uploads for a branch by running:
3770 git config branch.branch_name.skip-deps-uploads True
3771 To unset run:
3772 git config --unset branch.branch_name.skip-deps-uploads
3773 Can also set the above globally by using the --global flag.
3774 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003775 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3776 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003777 parser.add_option('--bypass-watchlists', action='store_true',
3778 dest='bypass_watchlists',
3779 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003780 parser.add_option('-f', action='store_true', dest='force',
3781 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003782 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003783 parser.add_option('-b', '--bug',
3784 help='pre-populate the bug number(s) for this issue. '
3785 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003786 parser.add_option('--message-file', dest='message_file',
3787 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003788 parser.add_option('-t', dest='title',
3789 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003790 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003791 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003792 help='reviewer email addresses')
3793 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003794 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003795 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003796 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003797 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003798 parser.add_option('--emulate_svn_auto_props',
3799 '--emulate-svn-auto-props',
3800 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003801 dest="emulate_svn_auto_props",
3802 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003803 parser.add_option('-c', '--use-commit-queue', action='store_true',
3804 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003805 parser.add_option('--private', action='store_true',
3806 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003807 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003808 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003809 metavar='TARGET',
3810 help='Apply CL to remote ref TARGET. ' +
3811 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003812 parser.add_option('--squash', action='store_true',
3813 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003814 parser.add_option('--no-squash', action='store_true',
3815 help='Don\'t squash multiple commits into one ' +
3816 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003817 parser.add_option('--email', default=None,
3818 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003819 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3820 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003821 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3822 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003823 help='Send the patchset to do a CQ dry run right after '
3824 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003825 parser.add_option('--dependencies', action='store_true',
3826 help='Uploads CLs of all the local branches that depend on '
3827 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003828
rmistry@google.com2dd99862015-06-22 12:22:18 +00003829 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003830 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003831 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003832 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003833 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003834 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003835 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003836
sbc@chromium.org71437c02015-04-09 19:29:40 +00003837 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003838 return 1
3839
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003840 options.reviewers = cleanup_list(options.reviewers)
3841 options.cc = cleanup_list(options.cc)
3842
tandriib80458a2016-06-23 12:20:07 -07003843 if options.message_file:
3844 if options.message:
3845 parser.error('only one of --message and --message-file allowed.')
3846 options.message = gclient_utils.FileRead(options.message_file)
3847 options.message_file = None
3848
tandrii4d0545a2016-07-06 03:56:49 -07003849 if options.cq_dry_run and options.use_commit_queue:
3850 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3851
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003852 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3853 settings.GetIsGerrit()
3854
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003855 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003856 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003857
3858
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003859def IsSubmoduleMergeCommit(ref):
3860 # When submodules are added to the repo, we expect there to be a single
3861 # non-git-svn merge commit at remote HEAD with a signature comment.
3862 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003863 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003864 return RunGit(cmd) != ''
3865
3866
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003867def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003868 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003869
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003870 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3871 upstream and closes the issue automatically and atomically.
3872
3873 Otherwise (in case of Rietveld):
3874 Squashes branch into a single commit.
3875 Updates changelog with metadata (e.g. pointer to review).
3876 Pushes/dcommits the code upstream.
3877 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003878 """
3879 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3880 help='bypass upload presubmit hook')
3881 parser.add_option('-m', dest='message',
3882 help="override review description")
3883 parser.add_option('-f', action='store_true', dest='force',
3884 help="force yes to questions (don't prompt)")
3885 parser.add_option('-c', dest='contributor',
3886 help="external contributor for patch (appended to " +
3887 "description and used as author for git). Should be " +
3888 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003889 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003890 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003891 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003892 auth_config = auth.extract_auth_config_from_options(options)
3893
3894 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003895
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003896 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3897 if cl.IsGerrit():
3898 if options.message:
3899 # This could be implemented, but it requires sending a new patch to
3900 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3901 # Besides, Gerrit has the ability to change the commit message on submit
3902 # automatically, thus there is no need to support this option (so far?).
3903 parser.error('-m MESSAGE option is not supported for Gerrit.')
3904 if options.contributor:
3905 parser.error(
3906 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3907 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3908 'the contributor\'s "name <email>". If you can\'t upload such a '
3909 'commit for review, contact your repository admin and request'
3910 '"Forge-Author" permission.')
3911 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3912 options.verbose)
3913
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003914 current = cl.GetBranch()
3915 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3916 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003917 print()
3918 print('Attempting to push branch %r into another local branch!' % current)
3919 print()
3920 print('Either reparent this branch on top of origin/master:')
3921 print(' git reparent-branch --root')
3922 print()
3923 print('OR run `git rebase-update` if you think the parent branch is ')
3924 print('already committed.')
3925 print()
3926 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003927 return 1
3928
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003929 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003930 # Default to merging against our best guess of the upstream branch.
3931 args = [cl.GetUpstreamBranch()]
3932
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003933 if options.contributor:
3934 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003935 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003936 return 1
3937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003938 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003939 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940
sbc@chromium.org71437c02015-04-09 19:29:40 +00003941 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003942 return 1
3943
3944 # This rev-list syntax means "show all commits not in my branch that
3945 # are in base_branch".
3946 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3947 base_branch]).splitlines()
3948 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003949 print('Base branch "%s" has %d commits '
3950 'not in this branch.' % (base_branch, len(upstream_commits)))
3951 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003952 return 1
3953
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003954 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003955 svn_head = None
3956 if cmd == 'dcommit' or base_has_submodules:
3957 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3958 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003959
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003960 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003961 # If the base_head is a submodule merge commit, the first parent of the
3962 # base_head should be a git-svn commit, which is what we're interested in.
3963 base_svn_head = base_branch
3964 if base_has_submodules:
3965 base_svn_head += '^1'
3966
3967 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003968 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003969 print('This branch has %d additional commits not upstreamed yet.'
3970 % len(extra_commits.splitlines()))
3971 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3972 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003973 return 1
3974
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003975 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003976 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003977 author = None
3978 if options.contributor:
3979 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003980 hook_results = cl.RunHook(
3981 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003982 may_prompt=not options.force,
3983 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003984 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003985 if not hook_results.should_continue():
3986 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003988 # Check the tree status if the tree status URL is set.
3989 status = GetTreeStatus()
3990 if 'closed' == status:
3991 print('The tree is closed. Please wait for it to reopen. Use '
3992 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3993 return 1
3994 elif 'unknown' == status:
3995 print('Unable to determine tree status. Please verify manually and '
3996 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3997 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003999 change_desc = ChangeDescription(options.message)
4000 if not change_desc.description and cl.GetIssue():
4001 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004002
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004003 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004004 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004005 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004006 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004007 print('No description set.')
4008 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004009 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004011 # Keep a separate copy for the commit message, because the commit message
4012 # contains the link to the Rietveld issue, while the Rietveld message contains
4013 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004014 # Keep a separate copy for the commit message.
4015 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004016 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004017
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004018 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004019 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004020 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004021 # after it. Add a period on a new line to circumvent this. Also add a space
4022 # before the period to make sure that Gitiles continues to correctly resolve
4023 # the URL.
4024 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004025 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004026 commit_desc.append_footer('Patch from %s.' % options.contributor)
4027
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004028 print('Description:')
4029 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004030
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004031 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004032 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004033 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004034
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004035 # We want to squash all this branch's commits into one commit with the proper
4036 # description. We do this by doing a "reset --soft" to the base branch (which
4037 # keeps the working copy the same), then dcommitting that. If origin/master
4038 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4039 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004040 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004041 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4042 # Delete the branches if they exist.
4043 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4044 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4045 result = RunGitWithCode(showref_cmd)
4046 if result[0] == 0:
4047 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048
4049 # We might be in a directory that's present in this branch but not in the
4050 # trunk. Move up to the top of the tree so that git commands that expect a
4051 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004052 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053 if rel_base_path:
4054 os.chdir(rel_base_path)
4055
4056 # Stuff our change into the merge branch.
4057 # We wrap in a try...finally block so if anything goes wrong,
4058 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004059 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004060 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004061 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004062 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004064 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004065 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004066 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004067 RunGit(
4068 [
4069 'commit', '--author', options.contributor,
4070 '-m', commit_desc.description,
4071 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004072 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004073 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004074 if base_has_submodules:
4075 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4076 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4077 RunGit(['checkout', CHERRY_PICK_BRANCH])
4078 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004079 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004080 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004081 mirror = settings.GetGitMirror(remote)
4082 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004083 pending_prefix = settings.GetPendingRefPrefix()
4084 if not pending_prefix or branch.startswith(pending_prefix):
4085 # If not using refs/pending/heads/* at all, or target ref is already set
4086 # to pending, then push to the target ref directly.
4087 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004088 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004089 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004090 else:
4091 # Cherry-pick the change on top of pending ref and then push it.
4092 assert branch.startswith('refs/'), branch
4093 assert pending_prefix[-1] == '/', pending_prefix
4094 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004095 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004096 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004097 if retcode == 0:
4098 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004099 else:
4100 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004101 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004102 'svn', 'dcommit',
4103 '-C%s' % options.similarity,
4104 '--no-rebase', '--rmdir',
4105 ]
4106 if settings.GetForceHttpsCommitUrl():
4107 # Allow forcing https commit URLs for some projects that don't allow
4108 # committing to http URLs (like Google Code).
4109 remote_url = cl.GetGitSvnRemoteUrl()
4110 if urlparse.urlparse(remote_url).scheme == 'http':
4111 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004112 cmd_args.append('--commit-url=%s' % remote_url)
4113 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004114 if 'Committed r' in output:
4115 revision = re.match(
4116 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4117 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004118 finally:
4119 # And then swap back to the original branch and clean up.
4120 RunGit(['checkout', '-q', cl.GetBranch()])
4121 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004122 if base_has_submodules:
4123 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004124
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004125 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004126 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004127 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004128
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004129 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004130 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004131 try:
4132 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4133 # We set pushed_to_pending to False, since it made it all the way to the
4134 # real ref.
4135 pushed_to_pending = False
4136 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004137 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004139 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004140 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004141 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004142 if not to_pending:
4143 if viewvc_url and revision:
4144 change_desc.append_footer(
4145 'Committed: %s%s' % (viewvc_url, revision))
4146 elif revision:
4147 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004148 print('Closing issue '
4149 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004150 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004151 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004152 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004153 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004154 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004155 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004156 if options.bypass_hooks:
4157 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4158 else:
4159 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004160 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004161
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004162 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004163 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004164 print('The commit is in the pending queue (%s).' % pending_ref)
4165 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4166 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004167
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004168 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4169 if os.path.isfile(hook):
4170 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004171
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004172 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004173
4174
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004175def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004176 print()
4177 print('Waiting for commit to be landed on %s...' % real_ref)
4178 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004179 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4180 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004181 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004182
4183 loop = 0
4184 while True:
4185 sys.stdout.write('fetching (%d)... \r' % loop)
4186 sys.stdout.flush()
4187 loop += 1
4188
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004189 if mirror:
4190 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004191 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4192 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4193 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4194 for commit in commits.splitlines():
4195 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004196 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004197 return commit
4198
4199 current_rev = to_rev
4200
4201
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004202def PushToGitPending(remote, pending_ref, upstream_ref):
4203 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4204
4205 Returns:
4206 (retcode of last operation, output log of last operation).
4207 """
4208 assert pending_ref.startswith('refs/'), pending_ref
4209 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4210 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4211 code = 0
4212 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004213 max_attempts = 3
4214 attempts_left = max_attempts
4215 while attempts_left:
4216 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004217 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004218 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004219
4220 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004221 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004222 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004223 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004224 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004225 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004226 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004227 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004228 continue
4229
4230 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004231 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004232 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004233 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004234 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004235 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4236 'the following files have merge conflicts:' % pending_ref)
4237 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4238 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004239 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004240 return code, out
4241
4242 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004243 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004244 code, out = RunGitWithCode(
4245 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4246 if code == 0:
4247 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004248 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004249 return code, out
4250
vapiera7fbd5a2016-06-16 09:17:49 -07004251 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004252 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004253 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004254 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004255 print('Fatal push error. Make sure your .netrc credentials and git '
4256 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004257 return code, out
4258
vapiera7fbd5a2016-06-16 09:17:49 -07004259 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004260 return code, out
4261
4262
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004263def IsFatalPushFailure(push_stdout):
4264 """True if retrying push won't help."""
4265 return '(prohibited by Gerrit)' in push_stdout
4266
4267
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004268@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004269def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004270 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004271 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004272 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004273 # If it looks like previous commits were mirrored with git-svn.
4274 message = """This repository appears to be a git-svn mirror, but no
4275upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4276 else:
4277 message = """This doesn't appear to be an SVN repository.
4278If your project has a true, writeable git repository, you probably want to run
4279'git cl land' instead.
4280If your project has a git mirror of an upstream SVN master, you probably need
4281to run 'git svn init'.
4282
4283Using the wrong command might cause your commit to appear to succeed, and the
4284review to be closed, without actually landing upstream. If you choose to
4285proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004286 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004287 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004288 # TODO(tandrii): kill this post SVN migration with
4289 # https://codereview.chromium.org/2076683002
4290 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4291 'Please let us know of this project you are committing to:'
4292 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004293 return SendUpstream(parser, args, 'dcommit')
4294
4295
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004296@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004297def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004298 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004299 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004300 print('This appears to be an SVN repository.')
4301 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004302 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004303 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004304 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004305
4306
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004307@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004308def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004309 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004310 parser.add_option('-b', dest='newbranch',
4311 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004312 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004313 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004314 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4315 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004316 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004317 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004318 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004319 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004320 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004321 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004322
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004323
4324 group = optparse.OptionGroup(
4325 parser,
4326 'Options for continuing work on the current issue uploaded from a '
4327 'different clone (e.g. different machine). Must be used independently '
4328 'from the other options. No issue number should be specified, and the '
4329 'branch must have an issue number associated with it')
4330 group.add_option('--reapply', action='store_true', dest='reapply',
4331 help='Reset the branch and reapply the issue.\n'
4332 'CAUTION: This will undo any local changes in this '
4333 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004334
4335 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004336 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004337 parser.add_option_group(group)
4338
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004339 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004340 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004341 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004342 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004343 auth_config = auth.extract_auth_config_from_options(options)
4344
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004345
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004346 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004347 if options.newbranch:
4348 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004349 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004350 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004351
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004352 cl = Changelist(auth_config=auth_config,
4353 codereview=options.forced_codereview)
4354 if not cl.GetIssue():
4355 parser.error('current branch must have an associated issue')
4356
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004357 upstream = cl.GetUpstreamBranch()
4358 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004359 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004360
4361 RunGit(['reset', '--hard', upstream])
4362 if options.pull:
4363 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004364
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004365 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4366 options.directory)
4367
4368 if len(args) != 1 or not args[0]:
4369 parser.error('Must specify issue number or url')
4370
4371 # We don't want uncommitted changes mixed up with the patch.
4372 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004373 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004374
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004375 if options.newbranch:
4376 if options.force:
4377 RunGit(['branch', '-D', options.newbranch],
4378 stderr=subprocess2.PIPE, error_ok=True)
4379 RunGit(['new-branch', options.newbranch])
4380
4381 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4382
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004383 if cl.IsGerrit():
4384 if options.reject:
4385 parser.error('--reject is not supported with Gerrit codereview.')
4386 if options.nocommit:
4387 parser.error('--nocommit is not supported with Gerrit codereview.')
4388 if options.directory:
4389 parser.error('--directory is not supported with Gerrit codereview.')
4390
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004391 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004392 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004393
4394
4395def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004396 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004397 # Provide a wrapper for git svn rebase to help avoid accidental
4398 # git svn dcommit.
4399 # It's the only command that doesn't use parser at all since we just defer
4400 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004401
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004402 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004403
4404
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004405def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406 """Fetches the tree status and returns either 'open', 'closed',
4407 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004408 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004409 if url:
4410 status = urllib2.urlopen(url).read().lower()
4411 if status.find('closed') != -1 or status == '0':
4412 return 'closed'
4413 elif status.find('open') != -1 or status == '1':
4414 return 'open'
4415 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004416 return 'unset'
4417
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004419def GetTreeStatusReason():
4420 """Fetches the tree status from a json url and returns the message
4421 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004422 url = settings.GetTreeStatusUrl()
4423 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004424 connection = urllib2.urlopen(json_url)
4425 status = json.loads(connection.read())
4426 connection.close()
4427 return status['message']
4428
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004429
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004430def GetBuilderMaster(bot_list):
4431 """For a given builder, fetch the master from AE if available."""
4432 map_url = 'https://builders-map.appspot.com/'
4433 try:
4434 master_map = json.load(urllib2.urlopen(map_url))
4435 except urllib2.URLError as e:
4436 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4437 (map_url, e))
4438 except ValueError as e:
4439 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4440 if not master_map:
4441 return None, 'Failed to build master map.'
4442
4443 result_master = ''
4444 for bot in bot_list:
4445 builder = bot.split(':', 1)[0]
4446 master_list = master_map.get(builder, [])
4447 if not master_list:
4448 return None, ('No matching master for builder %s.' % builder)
4449 elif len(master_list) > 1:
4450 return None, ('The builder name %s exists in multiple masters %s.' %
4451 (builder, master_list))
4452 else:
4453 cur_master = master_list[0]
4454 if not result_master:
4455 result_master = cur_master
4456 elif result_master != cur_master:
4457 return None, 'The builders do not belong to the same master.'
4458 return result_master, None
4459
4460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004461def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004462 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004463 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004464 status = GetTreeStatus()
4465 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004466 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004467 return 2
4468
vapiera7fbd5a2016-06-16 09:17:49 -07004469 print('The tree is %s' % status)
4470 print()
4471 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472 if status != 'open':
4473 return 1
4474 return 0
4475
4476
maruel@chromium.org15192402012-09-06 12:38:29 +00004477def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004478 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004479 group = optparse.OptionGroup(parser, "Try job options")
4480 group.add_option(
4481 "-b", "--bot", action="append",
4482 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4483 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004484 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004485 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004486 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004487 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004488 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004489 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004490 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004491 "-r", "--revision",
4492 help="Revision to use for the try job; default: the "
4493 "revision will be determined by the try server; see "
4494 "its waterfall for more info")
4495 group.add_option(
4496 "-c", "--clobber", action="store_true", default=False,
4497 help="Force a clobber before building; e.g. don't do an "
4498 "incremental build")
4499 group.add_option(
4500 "--project",
4501 help="Override which project to use. Projects are defined "
4502 "server-side to define what default bot set to use")
4503 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004504 "-p", "--property", dest="properties", action="append", default=[],
4505 help="Specify generic properties in the form -p key1=value1 -p "
4506 "key2=value2 etc (buildbucket only). The value will be treated as "
4507 "json if decodable, or as string otherwise.")
4508 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004509 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004510 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004511 "--use-rietveld", action="store_true", default=False,
4512 help="Use Rietveld to trigger try jobs.")
4513 group.add_option(
4514 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4515 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004516 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004517 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004518 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004519 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004520
machenbach@chromium.org45453142015-09-15 08:45:22 +00004521 if options.use_rietveld and options.properties:
4522 parser.error('Properties can only be specified with buildbucket')
4523
4524 # Make sure that all properties are prop=value pairs.
4525 bad_params = [x for x in options.properties if '=' not in x]
4526 if bad_params:
4527 parser.error('Got properties with missing "=": %s' % bad_params)
4528
maruel@chromium.org15192402012-09-06 12:38:29 +00004529 if args:
4530 parser.error('Unknown arguments: %s' % args)
4531
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004532 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004533 if not cl.GetIssue():
4534 parser.error('Need to upload first')
4535
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004536 if cl.IsGerrit():
4537 parser.error(
4538 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4539 'If your project has Commit Queue, dry run is a workaround:\n'
4540 ' git cl set-commit --dry-run')
4541 # Code below assumes Rietveld issue.
4542 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4543
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004544 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004545 if props.get('closed'):
4546 parser.error('Cannot send tryjobs for a closed CL')
4547
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004548 if props.get('private'):
4549 parser.error('Cannot use trybots with private issue')
4550
maruel@chromium.org15192402012-09-06 12:38:29 +00004551 if not options.name:
4552 options.name = cl.GetBranch()
4553
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004554 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004555 options.master, err_msg = GetBuilderMaster(options.bot)
4556 if err_msg:
4557 parser.error('Tryserver master cannot be found because: %s\n'
4558 'Please manually specify the tryserver master'
4559 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004560
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004561 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004562 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004563 if not options.bot:
4564 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004565
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004566 # Get try masters from PRESUBMIT.py files.
4567 masters = presubmit_support.DoGetTryMasters(
4568 change,
4569 change.LocalPaths(),
4570 settings.GetRoot(),
4571 None,
4572 None,
4573 options.verbose,
4574 sys.stdout)
4575 if masters:
4576 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004577
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004578 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4579 options.bot = presubmit_support.DoGetTrySlaves(
4580 change,
4581 change.LocalPaths(),
4582 settings.GetRoot(),
4583 None,
4584 None,
4585 options.verbose,
4586 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004587
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004588 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004589 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004590
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004591 builders_and_tests = {}
4592 # TODO(machenbach): The old style command-line options don't support
4593 # multiple try masters yet.
4594 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4595 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4596
4597 for bot in old_style:
4598 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004599 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004600 elif ',' in bot:
4601 parser.error('Specify one bot per --bot flag')
4602 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004603 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004604
4605 for bot, tests in new_style:
4606 builders_and_tests.setdefault(bot, []).extend(tests)
4607
4608 # Return a master map with one master to be backwards compatible. The
4609 # master name defaults to an empty string, which will cause the master
4610 # not to be set on rietveld (deprecated).
4611 return {options.master: builders_and_tests}
4612
4613 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004614 if not masters:
4615 # Default to triggering Dry Run (see http://crbug.com/625697).
4616 if options.verbose:
4617 print('git cl try with no bots now defaults to CQ Dry Run.')
4618 try:
4619 cl.SetCQState(_CQState.DRY_RUN)
4620 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4621 return 0
4622 except KeyboardInterrupt:
4623 raise
4624 except:
4625 print('WARNING: failed to trigger CQ Dry Run.\n'
4626 'Either:\n'
4627 ' * your project has no CQ\n'
4628 ' * you don\'t have permission to trigger Dry Run\n'
4629 ' * bug in this code (see stack trace below).\n'
4630 'Consider specifying which bots to trigger manually '
4631 'or asking your project owners for permissions '
4632 'or contacting Chrome Infrastructure team at '
4633 'https://www.chromium.org/infra\n\n')
4634 # Still raise exception so that stack trace is printed.
4635 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004636
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004637 for builders in masters.itervalues():
4638 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004639 print('ERROR You are trying to send a job to a triggered bot. This type '
4640 'of bot requires an\ninitial job from a parent (usually a builder).'
4641 ' Instead send your job to the parent.\n'
4642 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004643 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004644
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004645 patchset = cl.GetMostRecentPatchset()
4646 if patchset and patchset != cl.GetPatchset():
4647 print(
4648 '\nWARNING Mismatch between local config and server. Did a previous '
4649 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4650 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004651 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004652 try:
4653 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4654 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004655 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004656 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004657 except Exception as e:
4658 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004659 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4660 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004661 return 1
4662 else:
4663 try:
4664 cl.RpcServer().trigger_distributed_try_jobs(
4665 cl.GetIssue(), patchset, options.name, options.clobber,
4666 options.revision, masters)
4667 except urllib2.HTTPError as e:
4668 if e.code == 404:
4669 print('404 from rietveld; '
4670 'did you mean to use "git try" instead of "git cl try"?')
4671 return 1
4672 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004673
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004674 for (master, builders) in sorted(masters.iteritems()):
4675 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004676 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004677 length = max(len(builder) for builder in builders)
4678 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004679 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004680 return 0
4681
4682
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004683def CMDtry_results(parser, args):
4684 group = optparse.OptionGroup(parser, "Try job results options")
4685 group.add_option(
4686 "-p", "--patchset", type=int, help="patchset number if not current.")
4687 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004688 "--print-master", action='store_true', help="print master name as well.")
4689 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004690 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004691 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004692 group.add_option(
4693 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4694 help="Host of buildbucket. The default host is %default.")
4695 parser.add_option_group(group)
4696 auth.add_auth_options(parser)
4697 options, args = parser.parse_args(args)
4698 if args:
4699 parser.error('Unrecognized args: %s' % ' '.join(args))
4700
4701 auth_config = auth.extract_auth_config_from_options(options)
4702 cl = Changelist(auth_config=auth_config)
4703 if not cl.GetIssue():
4704 parser.error('Need to upload first')
4705
4706 if not options.patchset:
4707 options.patchset = cl.GetMostRecentPatchset()
4708 if options.patchset and options.patchset != cl.GetPatchset():
4709 print(
4710 '\nWARNING Mismatch between local config and server. Did a previous '
4711 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4712 'Continuing using\npatchset %s.\n' % options.patchset)
4713 try:
4714 jobs = fetch_try_jobs(auth_config, cl, options)
4715 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004716 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004717 return 1
4718 except Exception as e:
4719 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004720 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4721 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004722 return 1
4723 print_tryjobs(options, jobs)
4724 return 0
4725
4726
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004727@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004728def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004729 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004730 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004731 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004732 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004734 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004735 if args:
4736 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004737 branch = cl.GetBranch()
4738 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004739 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004740 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004741
4742 # Clear configured merge-base, if there is one.
4743 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004744 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004745 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004746 return 0
4747
4748
thestig@chromium.org00858c82013-12-02 23:08:03 +00004749def CMDweb(parser, args):
4750 """Opens the current CL in the web browser."""
4751 _, args = parser.parse_args(args)
4752 if args:
4753 parser.error('Unrecognized args: %s' % ' '.join(args))
4754
4755 issue_url = Changelist().GetIssueURL()
4756 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004757 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004758 return 1
4759
4760 webbrowser.open(issue_url)
4761 return 0
4762
4763
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004764def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004765 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004766 parser.add_option('-d', '--dry-run', action='store_true',
4767 help='trigger in dry run mode')
4768 parser.add_option('-c', '--clear', action='store_true',
4769 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004770 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004771 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004772 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004773 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004774 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004775 if args:
4776 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004777 if options.dry_run and options.clear:
4778 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4779
iannuccie53c9352016-08-17 14:40:40 -07004780 cl = Changelist(auth_config=auth_config, issue=options.issue,
4781 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004782 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004783 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004784 elif options.dry_run:
4785 state = _CQState.DRY_RUN
4786 else:
4787 state = _CQState.COMMIT
4788 if not cl.GetIssue():
4789 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004790 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004791 return 0
4792
4793
groby@chromium.org411034a2013-02-26 15:12:01 +00004794def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004795 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004796 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004797 auth.add_auth_options(parser)
4798 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004799 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004800 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004801 if args:
4802 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07004803 cl = Changelist(auth_config=auth_config, issue=options.issue,
4804 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00004805 # Ensure there actually is an issue to close.
4806 cl.GetDescription()
4807 cl.CloseIssue()
4808 return 0
4809
4810
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004811def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004812 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004813 auth.add_auth_options(parser)
4814 options, args = parser.parse_args(args)
4815 auth_config = auth.extract_auth_config_from_options(options)
4816 if args:
4817 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004818
4819 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004820 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004821 # Staged changes would be committed along with the patch from last
4822 # upload, hence counted toward the "last upload" side in the final
4823 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004824 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004825 return 1
4826
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004827 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004828 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004829 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004830 if not issue:
4831 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004832 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004833 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004834
4835 # Create a new branch based on the merge-base
4836 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004837 # Clear cached branch in cl object, to avoid overwriting original CL branch
4838 # properties.
4839 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004840 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004841 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004842 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004843 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004844 return rtn
4845
wychen@chromium.org06928532015-02-03 02:11:29 +00004846 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004847 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004848 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004849 finally:
4850 RunGit(['checkout', '-q', branch])
4851 RunGit(['branch', '-D', TMP_BRANCH])
4852
4853 return 0
4854
4855
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004856def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004857 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004858 parser.add_option(
4859 '--no-color',
4860 action='store_true',
4861 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004862 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004863 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004864 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004865
4866 author = RunGit(['config', 'user.email']).strip() or None
4867
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004868 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004869
4870 if args:
4871 if len(args) > 1:
4872 parser.error('Unknown args')
4873 base_branch = args[0]
4874 else:
4875 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004876 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004877
4878 change = cl.GetChange(base_branch, None)
4879 return owners_finder.OwnersFinder(
4880 [f.LocalPath() for f in
4881 cl.GetChange(base_branch, None).AffectedFiles()],
4882 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004883 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004884 disable_color=options.no_color).run()
4885
4886
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004887def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004888 """Generates a diff command."""
4889 # Generate diff for the current branch's changes.
4890 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4891 upstream_commit, '--' ]
4892
4893 if args:
4894 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004895 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004896 diff_cmd.append(arg)
4897 else:
4898 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004899
4900 return diff_cmd
4901
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004902def MatchingFileType(file_name, extensions):
4903 """Returns true if the file name ends with one of the given extensions."""
4904 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004905
enne@chromium.org555cfe42014-01-29 18:21:39 +00004906@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004907def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004908 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004909 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004910 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004911 parser.add_option('--full', action='store_true',
4912 help='Reformat the full content of all touched files')
4913 parser.add_option('--dry-run', action='store_true',
4914 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004915 parser.add_option('--python', action='store_true',
4916 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004917 parser.add_option('--diff', action='store_true',
4918 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004919 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004920
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004921 # git diff generates paths against the root of the repository. Change
4922 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004923 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004924 if rel_base_path:
4925 os.chdir(rel_base_path)
4926
digit@chromium.org29e47272013-05-17 17:01:46 +00004927 # Grab the merge-base commit, i.e. the upstream commit of the current
4928 # branch when it was created or the last time it was rebased. This is
4929 # to cover the case where the user may have called "git fetch origin",
4930 # moving the origin branch to a newer commit, but hasn't rebased yet.
4931 upstream_commit = None
4932 cl = Changelist()
4933 upstream_branch = cl.GetUpstreamBranch()
4934 if upstream_branch:
4935 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4936 upstream_commit = upstream_commit.strip()
4937
4938 if not upstream_commit:
4939 DieWithError('Could not find base commit for this branch. '
4940 'Are you in detached state?')
4941
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004942 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4943 diff_output = RunGit(changed_files_cmd)
4944 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004945 # Filter out files deleted by this CL
4946 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004947
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004948 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4949 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4950 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004951 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004952
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004953 top_dir = os.path.normpath(
4954 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4955
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004956 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4957 # formatted. This is used to block during the presubmit.
4958 return_value = 0
4959
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004960 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004961 # Locate the clang-format binary in the checkout
4962 try:
4963 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004964 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004965 DieWithError(e)
4966
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004967 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004968 cmd = [clang_format_tool]
4969 if not opts.dry_run and not opts.diff:
4970 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004971 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004972 if opts.diff:
4973 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004974 else:
4975 env = os.environ.copy()
4976 env['PATH'] = str(os.path.dirname(clang_format_tool))
4977 try:
4978 script = clang_format.FindClangFormatScriptInChromiumTree(
4979 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004980 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004981 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004982
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004983 cmd = [sys.executable, script, '-p0']
4984 if not opts.dry_run and not opts.diff:
4985 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004986
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004987 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4988 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004989
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004990 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4991 if opts.diff:
4992 sys.stdout.write(stdout)
4993 if opts.dry_run and len(stdout) > 0:
4994 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004995
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004996 # Similar code to above, but using yapf on .py files rather than clang-format
4997 # on C/C++ files
4998 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004999 yapf_tool = gclient_utils.FindExecutable('yapf')
5000 if yapf_tool is None:
5001 DieWithError('yapf not found in PATH')
5002
5003 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005004 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005005 cmd = [yapf_tool]
5006 if not opts.dry_run and not opts.diff:
5007 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005008 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005009 if opts.diff:
5010 sys.stdout.write(stdout)
5011 else:
5012 # TODO(sbc): yapf --lines mode still has some issues.
5013 # https://github.com/google/yapf/issues/154
5014 DieWithError('--python currently only works with --full')
5015
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005016 # Dart's formatter does not have the nice property of only operating on
5017 # modified chunks, so hard code full.
5018 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005019 try:
5020 command = [dart_format.FindDartFmtToolInChromiumTree()]
5021 if not opts.dry_run and not opts.diff:
5022 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005023 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005024
ppi@chromium.org6593d932016-03-03 15:41:15 +00005025 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005026 if opts.dry_run and stdout:
5027 return_value = 2
5028 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005029 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5030 'found in this checkout. Files in other languages are still '
5031 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005032
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005033 # Format GN build files. Always run on full build files for canonical form.
5034 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005035 cmd = ['gn', 'format' ]
5036 if opts.dry_run or opts.diff:
5037 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005038 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005039 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5040 shell=sys.platform == 'win32',
5041 cwd=top_dir)
5042 if opts.dry_run and gn_ret == 2:
5043 return_value = 2 # Not formatted.
5044 elif opts.diff and gn_ret == 2:
5045 # TODO this should compute and print the actual diff.
5046 print("This change has GN build file diff for " + gn_diff_file)
5047 elif gn_ret != 0:
5048 # For non-dry run cases (and non-2 return values for dry-run), a
5049 # nonzero error code indicates a failure, probably because the file
5050 # doesn't parse.
5051 DieWithError("gn format failed on " + gn_diff_file +
5052 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005053
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005054 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005055
5056
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005057@subcommand.usage('<codereview url or issue id>')
5058def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005059 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005060 _, args = parser.parse_args(args)
5061
5062 if len(args) != 1:
5063 parser.print_help()
5064 return 1
5065
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005066 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005067 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005068 parser.print_help()
5069 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005070 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005071
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005072 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005073 output = RunGit(['config', '--local', '--get-regexp',
5074 r'branch\..*\.%s' % issueprefix],
5075 error_ok=True)
5076 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005077 if issue == target_issue:
5078 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005079
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005080 branches = []
5081 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00005082 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005083 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005084 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005085 return 1
5086 if len(branches) == 1:
5087 RunGit(['checkout', branches[0]])
5088 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005089 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005090 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005091 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005092 which = raw_input('Choose by index: ')
5093 try:
5094 RunGit(['checkout', branches[int(which)]])
5095 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005096 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005097 return 1
5098
5099 return 0
5100
5101
maruel@chromium.org29404b52014-09-08 22:58:00 +00005102def CMDlol(parser, args):
5103 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005104 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005105 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5106 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5107 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005108 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005109 return 0
5110
5111
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005112class OptionParser(optparse.OptionParser):
5113 """Creates the option parse and add --verbose support."""
5114 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005115 optparse.OptionParser.__init__(
5116 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005117 self.add_option(
5118 '-v', '--verbose', action='count', default=0,
5119 help='Use 2 times for more debugging info')
5120
5121 def parse_args(self, args=None, values=None):
5122 options, args = optparse.OptionParser.parse_args(self, args, values)
5123 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5124 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5125 return options, args
5126
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005127
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005128def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005129 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005130 print('\nYour python version %s is unsupported, please upgrade.\n' %
5131 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005132 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005133
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005134 # Reload settings.
5135 global settings
5136 settings = Settings()
5137
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005138 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005139 dispatcher = subcommand.CommandDispatcher(__name__)
5140 try:
5141 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005142 except auth.AuthenticationError as e:
5143 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005144 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005145 if e.code != 500:
5146 raise
5147 DieWithError(
5148 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5149 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005150 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005151
5152
5153if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005154 # These affect sys.stdout so do it outside of main() to simplify mocks in
5155 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005156 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005157 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005158 try:
5159 sys.exit(main(sys.argv[1:]))
5160 except KeyboardInterrupt:
5161 sys.stderr.write('interrupted\n')
5162 sys.exit(1)