blob: a8eb59f5b8674ebad12448f1eea192bf1ccb358b [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'
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070087 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000134 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
machenbach@chromium.org45453142015-09-15 08:45:22 +0000283def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000284 rietveld_url = settings.GetDefaultServerUrl()
285 rietveld_host = urlparse.urlparse(rietveld_url).hostname
286 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
287 http = authenticator.authorize(httplib2.Http())
288 http.force_exception_to_status_code = True
289 issue_props = changelist.GetIssueProperties()
290 issue = changelist.GetIssue()
291 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000292 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000293
294 buildbucket_put_url = (
295 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000296 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
298 hostname=rietveld_host,
299 issue=issue,
300 patch=patchset)
301
302 batch_req_body = {'builds': []}
303 print_text = []
304 print_text.append('Tried jobs on:')
305 for master, builders_and_tests in sorted(masters.iteritems()):
306 print_text.append('Master: %s' % master)
307 bucket = _prefix_master(master)
308 for builder, tests in sorted(builders_and_tests.iteritems()):
309 print_text.append(' %s: %s' % (builder, tests))
310 parameters = {
311 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000312 'changes': [{
313 'author': {'email': issue_props['owner_email']},
314 'revision': options.revision,
315 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 'properties': {
317 'category': category,
318 'issue': issue,
319 'master': master,
320 'patch_project': issue_props['project'],
321 'patch_storage': 'rietveld',
322 'patchset': patchset,
323 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000324 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000325 },
326 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000327 if 'presubmit' in builder.lower():
328 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000329 if tests:
330 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000331 if properties:
332 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000333 if options.clobber:
334 parameters['properties']['clobber'] = True
335 batch_req_body['builds'].append(
336 {
337 'bucket': bucket,
338 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000339 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000340 'tags': ['builder:%s' % builder,
341 'buildset:%s' % buildset,
342 'master:%s' % master,
343 'user_agent:git_cl_try']
344 }
345 )
346
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000347 _buildbucket_retry(
348 'triggering tryjobs',
349 http,
350 buildbucket_put_url,
351 'PUT',
352 body=json.dumps(batch_req_body),
353 headers={'Content-Type': 'application/json'}
354 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000355 print_text.append('To see results here, run: git cl try-results')
356 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700357 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000358
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000359
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360def fetch_try_jobs(auth_config, changelist, options):
361 """Fetches tryjobs from buildbucket.
362
363 Returns a map from build id to build info as json dictionary.
364 """
365 rietveld_url = settings.GetDefaultServerUrl()
366 rietveld_host = urlparse.urlparse(rietveld_url).hostname
367 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
368 if authenticator.has_cached_credentials():
369 http = authenticator.authorize(httplib2.Http())
370 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700371 print('Warning: Some results might be missing because %s' %
372 # Get the message on how to login.
373 (auth.LoginRequiredError(rietveld_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000374 http = httplib2.Http()
375
376 http.force_exception_to_status_code = True
377
378 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
379 hostname=rietveld_host,
380 issue=changelist.GetIssue(),
381 patch=options.patchset)
382 params = {'tag': 'buildset:%s' % buildset}
383
384 builds = {}
385 while True:
386 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
387 hostname=options.buildbucket_host,
388 params=urllib.urlencode(params))
389 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
390 for build in content.get('builds', []):
391 builds[build['id']] = build
392 if 'next_cursor' in content:
393 params['start_cursor'] = content['next_cursor']
394 else:
395 break
396 return builds
397
398
399def print_tryjobs(options, builds):
400 """Prints nicely result of fetch_try_jobs."""
401 if not builds:
vapiera7fbd5a2016-06-16 09:17:49 -0700402 print('No tryjobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 return
404
405 # Make a copy, because we'll be modifying builds dictionary.
406 builds = builds.copy()
407 builder_names_cache = {}
408
409 def get_builder(b):
410 try:
411 return builder_names_cache[b['id']]
412 except KeyError:
413 try:
414 parameters = json.loads(b['parameters_json'])
415 name = parameters['builder_name']
416 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700417 print('WARNING: failed to get builder name for build %s: %s' % (
418 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000419 name = None
420 builder_names_cache[b['id']] = name
421 return name
422
423 def get_bucket(b):
424 bucket = b['bucket']
425 if bucket.startswith('master.'):
426 return bucket[len('master.'):]
427 return bucket
428
429 if options.print_master:
430 name_fmt = '%%-%ds %%-%ds' % (
431 max(len(str(get_bucket(b))) for b in builds.itervalues()),
432 max(len(str(get_builder(b))) for b in builds.itervalues()))
433 def get_name(b):
434 return name_fmt % (get_bucket(b), get_builder(b))
435 else:
436 name_fmt = '%%-%ds' % (
437 max(len(str(get_builder(b))) for b in builds.itervalues()))
438 def get_name(b):
439 return name_fmt % get_builder(b)
440
441 def sort_key(b):
442 return b['status'], b.get('result'), get_name(b), b.get('url')
443
444 def pop(title, f, color=None, **kwargs):
445 """Pop matching builds from `builds` dict and print them."""
446
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000447 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000448 colorize = str
449 else:
450 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
451
452 result = []
453 for b in builds.values():
454 if all(b.get(k) == v for k, v in kwargs.iteritems()):
455 builds.pop(b['id'])
456 result.append(b)
457 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700458 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700460 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461
462 total = len(builds)
463 pop(status='COMPLETED', result='SUCCESS',
464 title='Successes:', color=Fore.GREEN,
465 f=lambda b: (get_name(b), b.get('url')))
466 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
467 title='Infra Failures:', color=Fore.MAGENTA,
468 f=lambda b: (get_name(b), b.get('url')))
469 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
470 title='Failures:', color=Fore.RED,
471 f=lambda b: (get_name(b), b.get('url')))
472 pop(status='COMPLETED', result='CANCELED',
473 title='Canceled:', color=Fore.MAGENTA,
474 f=lambda b: (get_name(b),))
475 pop(status='COMPLETED', result='FAILURE',
476 failure_reason='INVALID_BUILD_DEFINITION',
477 title='Wrong master/builder name:', color=Fore.MAGENTA,
478 f=lambda b: (get_name(b),))
479 pop(status='COMPLETED', result='FAILURE',
480 title='Other failures:',
481 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
482 pop(status='COMPLETED',
483 title='Other finished:',
484 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
485 pop(status='STARTED',
486 title='Started:', color=Fore.YELLOW,
487 f=lambda b: (get_name(b), b.get('url')))
488 pop(status='SCHEDULED',
489 title='Scheduled:',
490 f=lambda b: (get_name(b), 'id=%s' % b['id']))
491 # The last section is just in case buildbucket API changes OR there is a bug.
492 pop(title='Other:',
493 f=lambda b: (get_name(b), 'id=%s' % b['id']))
494 assert len(builds) == 0
vapiera7fbd5a2016-06-16 09:17:49 -0700495 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000496
497
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000498def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
499 """Return the corresponding git ref if |base_url| together with |glob_spec|
500 matches the full |url|.
501
502 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
503 """
504 fetch_suburl, as_ref = glob_spec.split(':')
505 if allow_wildcards:
506 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
507 if glob_match:
508 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
509 # "branches/{472,597,648}/src:refs/remotes/svn/*".
510 branch_re = re.escape(base_url)
511 if glob_match.group(1):
512 branch_re += '/' + re.escape(glob_match.group(1))
513 wildcard = glob_match.group(2)
514 if wildcard == '*':
515 branch_re += '([^/]*)'
516 else:
517 # Escape and replace surrounding braces with parentheses and commas
518 # with pipe symbols.
519 wildcard = re.escape(wildcard)
520 wildcard = re.sub('^\\\\{', '(', wildcard)
521 wildcard = re.sub('\\\\,', '|', wildcard)
522 wildcard = re.sub('\\\\}$', ')', wildcard)
523 branch_re += wildcard
524 if glob_match.group(3):
525 branch_re += re.escape(glob_match.group(3))
526 match = re.match(branch_re, url)
527 if match:
528 return re.sub('\*$', match.group(1), as_ref)
529
530 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
531 if fetch_suburl:
532 full_url = base_url + '/' + fetch_suburl
533 else:
534 full_url = base_url
535 if full_url == url:
536 return as_ref
537 return None
538
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000539
iannucci@chromium.org79540052012-10-19 23:15:26 +0000540def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000541 """Prints statistics about the change to the user."""
542 # --no-ext-diff is broken in some versions of Git, so try to work around
543 # this by overriding the environment (but there is still a problem if the
544 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000545 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000546 if 'GIT_EXTERNAL_DIFF' in env:
547 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000548
549 if find_copies:
550 similarity_options = ['--find-copies-harder', '-l100000',
551 '-C%s' % similarity]
552 else:
553 similarity_options = ['-M%s' % similarity]
554
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000555 try:
556 stdout = sys.stdout.fileno()
557 except AttributeError:
558 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000559 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000560 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000561 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000562 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000563
564
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000565class BuildbucketResponseException(Exception):
566 pass
567
568
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569class Settings(object):
570 def __init__(self):
571 self.default_server = None
572 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000573 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574 self.is_git_svn = None
575 self.svn_branch = None
576 self.tree_status_url = None
577 self.viewvc_url = None
578 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000579 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000580 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000581 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000582 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000583 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000584 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000585 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586
587 def LazyUpdateIfNeeded(self):
588 """Updates the settings from a codereview.settings file, if available."""
589 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000590 # The only value that actually changes the behavior is
591 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000592 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000593 error_ok=True
594 ).strip().lower()
595
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000597 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598 LoadCodereviewSettingsFromFile(cr_settings_file)
599 self.updated = True
600
601 def GetDefaultServerUrl(self, error_ok=False):
602 if not self.default_server:
603 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000604 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000605 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 if error_ok:
607 return self.default_server
608 if not self.default_server:
609 error_message = ('Could not find settings file. You must configure '
610 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000611 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000612 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 return self.default_server
614
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000615 @staticmethod
616 def GetRelativeRoot():
617 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000620 if self.root is None:
621 self.root = os.path.abspath(self.GetRelativeRoot())
622 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000624 def GetGitMirror(self, remote='origin'):
625 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000626 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000627 if not os.path.isdir(local_url):
628 return None
629 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
630 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
631 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
632 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
633 if mirror.exists():
634 return mirror
635 return None
636
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 def GetIsGitSvn(self):
638 """Return true if this repo looks like it's using git-svn."""
639 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000640 if self.GetPendingRefPrefix():
641 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
642 self.is_git_svn = False
643 else:
644 # If you have any "svn-remote.*" config keys, we think you're using svn.
645 self.is_git_svn = RunGitWithCode(
646 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 return self.is_git_svn
648
649 def GetSVNBranch(self):
650 if self.svn_branch is None:
651 if not self.GetIsGitSvn():
652 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
653
654 # Try to figure out which remote branch we're based on.
655 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000656 # 1) iterate through our branch history and find the svn URL.
657 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658
659 # regexp matching the git-svn line that contains the URL.
660 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
661
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000662 # We don't want to go through all of history, so read a line from the
663 # pipe at a time.
664 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000665 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000666 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
667 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000668 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000669 for line in proc.stdout:
670 match = git_svn_re.match(line)
671 if match:
672 url = match.group(1)
673 proc.stdout.close() # Cut pipe.
674 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000676 if url:
677 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
678 remotes = RunGit(['config', '--get-regexp',
679 r'^svn-remote\..*\.url']).splitlines()
680 for remote in remotes:
681 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000682 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000683 remote = match.group(1)
684 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000685 rewrite_root = RunGit(
686 ['config', 'svn-remote.%s.rewriteRoot' % remote],
687 error_ok=True).strip()
688 if rewrite_root:
689 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000690 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000691 ['config', 'svn-remote.%s.fetch' % remote],
692 error_ok=True).strip()
693 if fetch_spec:
694 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
695 if self.svn_branch:
696 break
697 branch_spec = RunGit(
698 ['config', 'svn-remote.%s.branches' % remote],
699 error_ok=True).strip()
700 if branch_spec:
701 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
702 if self.svn_branch:
703 break
704 tag_spec = RunGit(
705 ['config', 'svn-remote.%s.tags' % remote],
706 error_ok=True).strip()
707 if tag_spec:
708 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
709 if self.svn_branch:
710 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
712 if not self.svn_branch:
713 DieWithError('Can\'t guess svn branch -- try specifying it on the '
714 'command line')
715
716 return self.svn_branch
717
718 def GetTreeStatusUrl(self, error_ok=False):
719 if not self.tree_status_url:
720 error_message = ('You must configure your tree status URL by running '
721 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000722 self.tree_status_url = self._GetRietveldConfig(
723 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 return self.tree_status_url
725
726 def GetViewVCUrl(self):
727 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000728 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 return self.viewvc_url
730
rmistry@google.com90752582014-01-14 21:04:50 +0000731 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000733
rmistry@google.com78948ed2015-07-08 23:09:57 +0000734 def GetIsSkipDependencyUpload(self, branch_name):
735 """Returns true if specified branch should skip dep uploads."""
736 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
737 error_ok=True)
738
rmistry@google.com5626a922015-02-26 14:03:30 +0000739 def GetRunPostUploadHook(self):
740 run_post_upload_hook = self._GetRietveldConfig(
741 'run-post-upload-hook', error_ok=True)
742 return run_post_upload_hook == "True"
743
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000744 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000745 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000746
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000747 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000748 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000749
ukai@chromium.orge8077812012-02-03 03:41:46 +0000750 def GetIsGerrit(self):
751 """Return true if this repo is assosiated with gerrit code review system."""
752 if self.is_gerrit is None:
753 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
754 return self.is_gerrit
755
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000756 def GetSquashGerritUploads(self):
757 """Return true if uploads to Gerrit should be squashed by default."""
758 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700759 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
760 if self.squash_gerrit_uploads is None:
761 # Default is squash now (http://crbug.com/611892#c23).
762 self.squash_gerrit_uploads = not (
763 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
764 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000765 return self.squash_gerrit_uploads
766
tandriia60502f2016-06-20 02:01:53 -0700767 def GetSquashGerritUploadsOverride(self):
768 """Return True or False if codereview.settings should be overridden.
769
770 Returns None if no override has been defined.
771 """
772 # See also http://crbug.com/611892#c23
773 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
774 error_ok=True).strip()
775 if result == 'true':
776 return True
777 if result == 'false':
778 return False
779 return None
780
tandrii@chromium.org28253532016-04-14 13:46:56 +0000781 def GetGerritSkipEnsureAuthenticated(self):
782 """Return True if EnsureAuthenticated should not be done for Gerrit
783 uploads."""
784 if self.gerrit_skip_ensure_authenticated is None:
785 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000786 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000787 error_ok=True).strip() == 'true')
788 return self.gerrit_skip_ensure_authenticated
789
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000790 def GetGitEditor(self):
791 """Return the editor specified in the git config, or None if none is."""
792 if self.git_editor is None:
793 self.git_editor = self._GetConfig('core.editor', error_ok=True)
794 return self.git_editor or None
795
thestig@chromium.org44202a22014-03-11 19:22:18 +0000796 def GetLintRegex(self):
797 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
798 DEFAULT_LINT_REGEX)
799
800 def GetLintIgnoreRegex(self):
801 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
802 DEFAULT_LINT_IGNORE_REGEX)
803
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000804 def GetProject(self):
805 if not self.project:
806 self.project = self._GetRietveldConfig('project', error_ok=True)
807 return self.project
808
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000809 def GetForceHttpsCommitUrl(self):
810 if not self.force_https_commit_url:
811 self.force_https_commit_url = self._GetRietveldConfig(
812 'force-https-commit-url', error_ok=True)
813 return self.force_https_commit_url
814
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000815 def GetPendingRefPrefix(self):
816 if not self.pending_ref_prefix:
817 self.pending_ref_prefix = self._GetRietveldConfig(
818 'pending-ref-prefix', error_ok=True)
819 return self.pending_ref_prefix
820
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000821 def _GetRietveldConfig(self, param, **kwargs):
822 return self._GetConfig('rietveld.' + param, **kwargs)
823
rmistry@google.com78948ed2015-07-08 23:09:57 +0000824 def _GetBranchConfig(self, branch_name, param, **kwargs):
825 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 def _GetConfig(self, param, **kwargs):
828 self.LazyUpdateIfNeeded()
829 return RunGit(['config', param], **kwargs).strip()
830
831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832def ShortBranchName(branch):
833 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000834 return branch.replace('refs/heads/', '', 1)
835
836
837def GetCurrentBranchRef():
838 """Returns branch ref (e.g., refs/heads/master) or None."""
839 return RunGit(['symbolic-ref', 'HEAD'],
840 stderr=subprocess2.VOID, error_ok=True).strip() or None
841
842
843def GetCurrentBranch():
844 """Returns current branch or None.
845
846 For refs/heads/* branches, returns just last part. For others, full ref.
847 """
848 branchref = GetCurrentBranchRef()
849 if branchref:
850 return ShortBranchName(branchref)
851 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
853
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000854class _CQState(object):
855 """Enum for states of CL with respect to Commit Queue."""
856 NONE = 'none'
857 DRY_RUN = 'dry_run'
858 COMMIT = 'commit'
859
860 ALL_STATES = [NONE, DRY_RUN, COMMIT]
861
862
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000863class _ParsedIssueNumberArgument(object):
864 def __init__(self, issue=None, patchset=None, hostname=None):
865 self.issue = issue
866 self.patchset = patchset
867 self.hostname = hostname
868
869 @property
870 def valid(self):
871 return self.issue is not None
872
873
874class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
875 def __init__(self, *args, **kwargs):
876 self.patch_url = kwargs.pop('patch_url', None)
877 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
878
879
880def ParseIssueNumberArgument(arg):
881 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
882 fail_result = _ParsedIssueNumberArgument()
883
884 if arg.isdigit():
885 return _ParsedIssueNumberArgument(issue=int(arg))
886 if not arg.startswith('http'):
887 return fail_result
888 url = gclient_utils.UpgradeToHttps(arg)
889 try:
890 parsed_url = urlparse.urlparse(url)
891 except ValueError:
892 return fail_result
893 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
894 tmp = cls.ParseIssueURL(parsed_url)
895 if tmp is not None:
896 return tmp
897 return fail_result
898
899
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901 """Changelist works with one changelist in local branch.
902
903 Supports two codereview backends: Rietveld or Gerrit, selected at object
904 creation.
905
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000906 Notes:
907 * Not safe for concurrent multi-{thread,process} use.
908 * Caches values from current branch. Therefore, re-use after branch change
909 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000910 """
911
912 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
913 """Create a new ChangeList instance.
914
915 If issue is given, the codereview must be given too.
916
917 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
918 Otherwise, it's decided based on current configuration of the local branch,
919 with default being 'rietveld' for backwards compatibility.
920 See _load_codereview_impl for more details.
921
922 **kwargs will be passed directly to codereview implementation.
923 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000925 global settings
926 if not settings:
927 # Happens when git_cl.py is used as a utility library.
928 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000929
930 if issue:
931 assert codereview, 'codereview must be known, if issue is known'
932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000933 self.branchref = branchref
934 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000935 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 self.branch = ShortBranchName(self.branchref)
937 else:
938 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000940 self.lookedup_issue = False
941 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942 self.has_description = False
943 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000944 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000946 self.cc = None
947 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000948 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000949
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000950 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000951 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000952 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000953 assert self._codereview_impl
954 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000955
956 def _load_codereview_impl(self, codereview=None, **kwargs):
957 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000958 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
959 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
960 self._codereview = codereview
961 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000962 return
963
964 # Automatic selection based on issue number set for a current branch.
965 # Rietveld takes precedence over Gerrit.
966 assert not self.issue
967 # Whether we find issue or not, we are doing the lookup.
968 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000969 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970 setting = cls.IssueSetting(self.GetBranch())
971 issue = RunGit(['config', setting], error_ok=True).strip()
972 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000973 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000974 self._codereview_impl = cls(self, **kwargs)
975 self.issue = int(issue)
976 return
977
978 # No issue is set for this branch, so decide based on repo-wide settings.
979 return self._load_codereview_impl(
980 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
981 **kwargs)
982
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000983 def IsGerrit(self):
984 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000985
986 def GetCCList(self):
987 """Return the users cc'd on this CL.
988
989 Return is a string suitable for passing to gcl with the --cc flag.
990 """
991 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000992 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000993 more_cc = ','.join(self.watchers)
994 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
995 return self.cc
996
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000997 def GetCCListWithoutDefault(self):
998 """Return the users cc'd on this CL excluding default ones."""
999 if self.cc is None:
1000 self.cc = ','.join(self.watchers)
1001 return self.cc
1002
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001003 def SetWatchers(self, watchers):
1004 """Set the list of email addresses that should be cc'd based on the changed
1005 files in this CL.
1006 """
1007 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008
1009 def GetBranch(self):
1010 """Returns the short branch name, e.g. 'master'."""
1011 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001012 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001013 if not branchref:
1014 return None
1015 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016 self.branch = ShortBranchName(self.branchref)
1017 return self.branch
1018
1019 def GetBranchRef(self):
1020 """Returns the full branch name, e.g. 'refs/heads/master'."""
1021 self.GetBranch() # Poke the lazy loader.
1022 return self.branchref
1023
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001024 def ClearBranch(self):
1025 """Clears cached branch data of this object."""
1026 self.branch = self.branchref = None
1027
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001028 @staticmethod
1029 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001030 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 e.g. 'origin', 'refs/heads/master'
1032 """
1033 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001034 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1035 error_ok=True).strip()
1036 if upstream_branch:
1037 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1038 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001039 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1040 error_ok=True).strip()
1041 if upstream_branch:
1042 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001044 # Fall back on trying a git-svn upstream branch.
1045 if settings.GetIsGitSvn():
1046 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001048 # Else, try to guess the origin remote.
1049 remote_branches = RunGit(['branch', '-r']).split()
1050 if 'origin/master' in remote_branches:
1051 # Fall back on origin/master if it exits.
1052 remote = 'origin'
1053 upstream_branch = 'refs/heads/master'
1054 elif 'origin/trunk' in remote_branches:
1055 # Fall back on origin/trunk if it exists. Generally a shared
1056 # git-svn clone
1057 remote = 'origin'
1058 upstream_branch = 'refs/heads/trunk'
1059 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001060 DieWithError(
1061 'Unable to determine default branch to diff against.\n'
1062 'Either pass complete "git diff"-style arguments, like\n'
1063 ' git cl upload origin/master\n'
1064 'or verify this branch is set up to track another \n'
1065 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066
1067 return remote, upstream_branch
1068
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001069 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001070 upstream_branch = self.GetUpstreamBranch()
1071 if not BranchExists(upstream_branch):
1072 DieWithError('The upstream for the current branch (%s) does not exist '
1073 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001074 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001075 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 def GetUpstreamBranch(self):
1078 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001079 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001081 upstream_branch = upstream_branch.replace('refs/heads/',
1082 'refs/remotes/%s/' % remote)
1083 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1084 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085 self.upstream_branch = upstream_branch
1086 return self.upstream_branch
1087
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001088 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001089 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001090 remote, branch = None, self.GetBranch()
1091 seen_branches = set()
1092 while branch not in seen_branches:
1093 seen_branches.add(branch)
1094 remote, branch = self.FetchUpstreamTuple(branch)
1095 branch = ShortBranchName(branch)
1096 if remote != '.' or branch.startswith('refs/remotes'):
1097 break
1098 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001099 remotes = RunGit(['remote'], error_ok=True).split()
1100 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001101 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001102 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001104 logging.warning('Could not determine which remote this change is '
1105 'associated with, so defaulting to "%s". This may '
1106 'not be what you want. You may prevent this message '
1107 'by running "git svn info" as documented here: %s',
1108 self._remote,
1109 GIT_INSTRUCTIONS_URL)
1110 else:
1111 logging.warn('Could not determine which remote this change is '
1112 'associated with. You may prevent this message by '
1113 'running "git svn info" as documented here: %s',
1114 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 branch = 'HEAD'
1116 if branch.startswith('refs/remotes'):
1117 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001118 elif branch.startswith('refs/branch-heads/'):
1119 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001120 else:
1121 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001122 return self._remote
1123
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001124 def GitSanityChecks(self, upstream_git_obj):
1125 """Checks git repo status and ensures diff is from local commits."""
1126
sbc@chromium.org79706062015-01-14 21:18:12 +00001127 if upstream_git_obj is None:
1128 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001129 print('ERROR: unable to determine current branch (detached HEAD?)',
1130 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001131 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001132 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001133 return False
1134
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001135 # Verify the commit we're diffing against is in our current branch.
1136 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1137 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1138 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001139 print('ERROR: %s is not in the current branch. You may need to rebase '
1140 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001141 return False
1142
1143 # List the commits inside the diff, and verify they are all local.
1144 commits_in_diff = RunGit(
1145 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1146 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1147 remote_branch = remote_branch.strip()
1148 if code != 0:
1149 _, remote_branch = self.GetRemoteBranch()
1150
1151 commits_in_remote = RunGit(
1152 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1153
1154 common_commits = set(commits_in_diff) & set(commits_in_remote)
1155 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001156 print('ERROR: Your diff contains %d commits already in %s.\n'
1157 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1158 'the diff. If you are using a custom git flow, you can override'
1159 ' the reference used for this check with "git config '
1160 'gitcl.remotebranch <git-ref>".' % (
1161 len(common_commits), remote_branch, upstream_git_obj),
1162 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001163 return False
1164 return True
1165
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001166 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001167 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001168
1169 Returns None if it is not set.
1170 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001171 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1172 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001173
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001174 def GetGitSvnRemoteUrl(self):
1175 """Return the configured git-svn remote URL parsed from git svn info.
1176
1177 Returns None if it is not set.
1178 """
1179 # URL is dependent on the current directory.
1180 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1181 if data:
1182 keys = dict(line.split(': ', 1) for line in data.splitlines()
1183 if ': ' in line)
1184 return keys.get('URL', None)
1185 return None
1186
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 def GetRemoteUrl(self):
1188 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1189
1190 Returns None if there is no remote.
1191 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001192 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001193 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1194
1195 # If URL is pointing to a local directory, it is probably a git cache.
1196 if os.path.isdir(url):
1197 url = RunGit(['config', 'remote.%s.url' % remote],
1198 error_ok=True,
1199 cwd=url).strip()
1200 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001202 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001203 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001204 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001205 issue = RunGit(['config',
1206 self._codereview_impl.IssueSetting(self.GetBranch())],
1207 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 self.issue = int(issue) or None if issue else None
1209 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return self.issue
1211
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 def GetIssueURL(self):
1213 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 issue = self.GetIssue()
1215 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001216 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001217 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
1219 def GetDescription(self, pretty=False):
1220 if not self.has_description:
1221 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001222 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 self.has_description = True
1224 if pretty:
1225 wrapper = textwrap.TextWrapper()
1226 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1227 return wrapper.fill(self.description)
1228 return self.description
1229
1230 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001231 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001233 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001235 self.patchset = int(patchset) or None if patchset else None
1236 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 return self.patchset
1238
1239 def SetPatchset(self, patchset):
1240 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001243 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001244 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001246 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001247 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001248 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001250 def SetIssue(self, issue=None):
1251 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001252 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1253 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001255 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001256 RunGit(['config', issue_setting, str(issue)])
1257 codereview_server = self._codereview_impl.GetCodereviewServer()
1258 if codereview_server:
1259 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001261 # Reset it regardless. It doesn't hurt.
1262 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1263 for prop in (['last-upload-hash'] +
1264 self._codereview_impl._PostUnsetIssueProperties()):
1265 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1266 for setting in config_settings:
1267 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001268 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001269 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001271 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001272 if not self.GitSanityChecks(upstream_branch):
1273 DieWithError('\nGit sanity check failure')
1274
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001275 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001276 if not root:
1277 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001278 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001279
1280 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001281 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001282 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001283 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001284 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001285 except subprocess2.CalledProcessError:
1286 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001287 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001288 'This branch probably doesn\'t exist anymore. To reset the\n'
1289 'tracking branch, please run\n'
1290 ' git branch --set-upstream %s trunk\n'
1291 'replacing trunk with origin/master or the relevant branch') %
1292 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001293
maruel@chromium.org52424302012-08-29 15:14:30 +00001294 issue = self.GetIssue()
1295 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001296 if issue:
1297 description = self.GetDescription()
1298 else:
1299 # If the change was never uploaded, use the log messages of all commits
1300 # up to the branch point, as git cl upload will prefill the description
1301 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001302 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1303 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001304
1305 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001306 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001307 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001308 name,
1309 description,
1310 absroot,
1311 files,
1312 issue,
1313 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001314 author,
1315 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001316
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001317 def UpdateDescription(self, description):
1318 self.description = description
1319 return self._codereview_impl.UpdateDescriptionRemote(description)
1320
1321 def RunHook(self, committing, may_prompt, verbose, change):
1322 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1323 try:
1324 return presubmit_support.DoPresubmitChecks(change, committing,
1325 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1326 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001327 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1328 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001329 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001330 DieWithError(
1331 ('%s\nMaybe your depot_tools is out of date?\n'
1332 'If all fails, contact maruel@') % e)
1333
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001334 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1335 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001336 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1337 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001338 else:
1339 # Assume url.
1340 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1341 urlparse.urlparse(issue_arg))
1342 if not parsed_issue_arg or not parsed_issue_arg.valid:
1343 DieWithError('Failed to parse issue argument "%s". '
1344 'Must be an issue number or a valid URL.' % issue_arg)
1345 return self._codereview_impl.CMDPatchWithParsedIssue(
1346 parsed_issue_arg, reject, nocommit, directory)
1347
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001348 def CMDUpload(self, options, git_diff_args, orig_args):
1349 """Uploads a change to codereview."""
1350 if git_diff_args:
1351 # TODO(ukai): is it ok for gerrit case?
1352 base_branch = git_diff_args[0]
1353 else:
1354 if self.GetBranch() is None:
1355 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1356
1357 # Default to diffing against common ancestor of upstream branch
1358 base_branch = self.GetCommonAncestorWithUpstream()
1359 git_diff_args = [base_branch, 'HEAD']
1360
1361 # Make sure authenticated to codereview before running potentially expensive
1362 # hooks. It is a fast, best efforts check. Codereview still can reject the
1363 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001364 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001365
1366 # Apply watchlists on upload.
1367 change = self.GetChange(base_branch, None)
1368 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1369 files = [f.LocalPath() for f in change.AffectedFiles()]
1370 if not options.bypass_watchlists:
1371 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1372
1373 if not options.bypass_hooks:
1374 if options.reviewers or options.tbr_owners:
1375 # Set the reviewer list now so that presubmit checks can access it.
1376 change_description = ChangeDescription(change.FullDescriptionText())
1377 change_description.update_reviewers(options.reviewers,
1378 options.tbr_owners,
1379 change)
1380 change.SetDescriptionText(change_description.description)
1381 hook_results = self.RunHook(committing=False,
1382 may_prompt=not options.force,
1383 verbose=options.verbose,
1384 change=change)
1385 if not hook_results.should_continue():
1386 return 1
1387 if not options.reviewers and hook_results.reviewers:
1388 options.reviewers = hook_results.reviewers.split(',')
1389
1390 if self.GetIssue():
1391 latest_patchset = self.GetMostRecentPatchset()
1392 local_patchset = self.GetPatchset()
1393 if (latest_patchset and local_patchset and
1394 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001395 print('The last upload made from this repository was patchset #%d but '
1396 'the most recent patchset on the server is #%d.'
1397 % (local_patchset, latest_patchset))
1398 print('Uploading will still work, but if you\'ve uploaded to this '
1399 'issue from another machine or branch the patch you\'re '
1400 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001401 ask_for_data('About to upload; enter to confirm.')
1402
1403 print_stats(options.similarity, options.find_copies, git_diff_args)
1404 ret = self.CMDUploadChange(options, git_diff_args, change)
1405 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001406 if options.use_commit_queue:
1407 self.SetCQState(_CQState.COMMIT)
1408 elif options.cq_dry_run:
1409 self.SetCQState(_CQState.DRY_RUN)
1410
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001411 git_set_branch_value('last-upload-hash',
1412 RunGit(['rev-parse', 'HEAD']).strip())
1413 # Run post upload hooks, if specified.
1414 if settings.GetRunPostUploadHook():
1415 presubmit_support.DoPostUploadExecuter(
1416 change,
1417 self,
1418 settings.GetRoot(),
1419 options.verbose,
1420 sys.stdout)
1421
1422 # Upload all dependencies if specified.
1423 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001424 print()
1425 print('--dependencies has been specified.')
1426 print('All dependent local branches will be re-uploaded.')
1427 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001428 # Remove the dependencies flag from args so that we do not end up in a
1429 # loop.
1430 orig_args.remove('--dependencies')
1431 ret = upload_branch_deps(self, orig_args)
1432 return ret
1433
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001434 def SetCQState(self, new_state):
1435 """Update the CQ state for latest patchset.
1436
1437 Issue must have been already uploaded and known.
1438 """
1439 assert new_state in _CQState.ALL_STATES
1440 assert self.GetIssue()
1441 return self._codereview_impl.SetCQState(new_state)
1442
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001443 # Forward methods to codereview specific implementation.
1444
1445 def CloseIssue(self):
1446 return self._codereview_impl.CloseIssue()
1447
1448 def GetStatus(self):
1449 return self._codereview_impl.GetStatus()
1450
1451 def GetCodereviewServer(self):
1452 return self._codereview_impl.GetCodereviewServer()
1453
1454 def GetApprovingReviewers(self):
1455 return self._codereview_impl.GetApprovingReviewers()
1456
1457 def GetMostRecentPatchset(self):
1458 return self._codereview_impl.GetMostRecentPatchset()
1459
1460 def __getattr__(self, attr):
1461 # This is because lots of untested code accesses Rietveld-specific stuff
1462 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001463 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001464 return getattr(self._codereview_impl, attr)
1465
1466
1467class _ChangelistCodereviewBase(object):
1468 """Abstract base class encapsulating codereview specifics of a changelist."""
1469 def __init__(self, changelist):
1470 self._changelist = changelist # instance of Changelist
1471
1472 def __getattr__(self, attr):
1473 # Forward methods to changelist.
1474 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1475 # _RietveldChangelistImpl to avoid this hack?
1476 return getattr(self._changelist, attr)
1477
1478 def GetStatus(self):
1479 """Apply a rough heuristic to give a simple summary of an issue's review
1480 or CQ status, assuming adherence to a common workflow.
1481
1482 Returns None if no issue for this branch, or specific string keywords.
1483 """
1484 raise NotImplementedError()
1485
1486 def GetCodereviewServer(self):
1487 """Returns server URL without end slash, like "https://codereview.com"."""
1488 raise NotImplementedError()
1489
1490 def FetchDescription(self):
1491 """Fetches and returns description from the codereview server."""
1492 raise NotImplementedError()
1493
1494 def GetCodereviewServerSetting(self):
1495 """Returns git config setting for the codereview server."""
1496 raise NotImplementedError()
1497
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001498 @classmethod
1499 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001500 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001501
1502 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001503 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001504 """Returns name of git config setting which stores issue number for a given
1505 branch."""
1506 raise NotImplementedError()
1507
1508 def PatchsetSetting(self):
1509 """Returns name of git config setting which stores issue number."""
1510 raise NotImplementedError()
1511
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001512 def _PostUnsetIssueProperties(self):
1513 """Which branch-specific properties to erase when unsettin issue."""
1514 raise NotImplementedError()
1515
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001516 def GetRieveldObjForPresubmit(self):
1517 # This is an unfortunate Rietveld-embeddedness in presubmit.
1518 # For non-Rietveld codereviews, this probably should return a dummy object.
1519 raise NotImplementedError()
1520
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001521 def GetGerritObjForPresubmit(self):
1522 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1523 return None
1524
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001525 def UpdateDescriptionRemote(self, description):
1526 """Update the description on codereview site."""
1527 raise NotImplementedError()
1528
1529 def CloseIssue(self):
1530 """Closes the issue."""
1531 raise NotImplementedError()
1532
1533 def GetApprovingReviewers(self):
1534 """Returns a list of reviewers approving the change.
1535
1536 Note: not necessarily committers.
1537 """
1538 raise NotImplementedError()
1539
1540 def GetMostRecentPatchset(self):
1541 """Returns the most recent patchset number from the codereview site."""
1542 raise NotImplementedError()
1543
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001544 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1545 directory):
1546 """Fetches and applies the issue.
1547
1548 Arguments:
1549 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1550 reject: if True, reject the failed patch instead of switching to 3-way
1551 merge. Rietveld only.
1552 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1553 only.
1554 directory: switch to directory before applying the patch. Rietveld only.
1555 """
1556 raise NotImplementedError()
1557
1558 @staticmethod
1559 def ParseIssueURL(parsed_url):
1560 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1561 failed."""
1562 raise NotImplementedError()
1563
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001564 def EnsureAuthenticated(self, force):
1565 """Best effort check that user is authenticated with codereview server.
1566
1567 Arguments:
1568 force: whether to skip confirmation questions.
1569 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001570 raise NotImplementedError()
1571
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001572 def CMDUploadChange(self, options, args, change):
1573 """Uploads a change to codereview."""
1574 raise NotImplementedError()
1575
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001576 def SetCQState(self, new_state):
1577 """Update the CQ state for latest patchset.
1578
1579 Issue must have been already uploaded and known.
1580 """
1581 raise NotImplementedError()
1582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583
1584class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1585 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1586 super(_RietveldChangelistImpl, self).__init__(changelist)
1587 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001588 if not rietveld_server:
1589 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590
1591 self._rietveld_server = rietveld_server
1592 self._auth_config = auth_config
1593 self._props = None
1594 self._rpc_server = None
1595
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001596 def GetCodereviewServer(self):
1597 if not self._rietveld_server:
1598 # If we're on a branch then get the server potentially associated
1599 # with that branch.
1600 if self.GetIssue():
1601 rietveld_server_setting = self.GetCodereviewServerSetting()
1602 if rietveld_server_setting:
1603 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1604 ['config', rietveld_server_setting], error_ok=True).strip())
1605 if not self._rietveld_server:
1606 self._rietveld_server = settings.GetDefaultServerUrl()
1607 return self._rietveld_server
1608
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001609 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610 """Best effort check that user is authenticated with Rietveld server."""
1611 if self._auth_config.use_oauth2:
1612 authenticator = auth.get_authenticator_for_host(
1613 self.GetCodereviewServer(), self._auth_config)
1614 if not authenticator.has_cached_credentials():
1615 raise auth.LoginRequiredError(self.GetCodereviewServer())
1616
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001617 def FetchDescription(self):
1618 issue = self.GetIssue()
1619 assert issue
1620 try:
1621 return self.RpcServer().get_description(issue).strip()
1622 except urllib2.HTTPError as e:
1623 if e.code == 404:
1624 DieWithError(
1625 ('\nWhile fetching the description for issue %d, received a '
1626 '404 (not found)\n'
1627 'error. It is likely that you deleted this '
1628 'issue on the server. If this is the\n'
1629 'case, please run\n\n'
1630 ' git cl issue 0\n\n'
1631 'to clear the association with the deleted issue. Then run '
1632 'this command again.') % issue)
1633 else:
1634 DieWithError(
1635 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1636 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001637 print('Warning: Failed to retrieve CL description due to network '
1638 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001639 return ''
1640
1641 def GetMostRecentPatchset(self):
1642 return self.GetIssueProperties()['patchsets'][-1]
1643
1644 def GetPatchSetDiff(self, issue, patchset):
1645 return self.RpcServer().get(
1646 '/download/issue%s_%s.diff' % (issue, patchset))
1647
1648 def GetIssueProperties(self):
1649 if self._props is None:
1650 issue = self.GetIssue()
1651 if not issue:
1652 self._props = {}
1653 else:
1654 self._props = self.RpcServer().get_issue_properties(issue, True)
1655 return self._props
1656
1657 def GetApprovingReviewers(self):
1658 return get_approving_reviewers(self.GetIssueProperties())
1659
1660 def AddComment(self, message):
1661 return self.RpcServer().add_comment(self.GetIssue(), message)
1662
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001663 def GetStatus(self):
1664 """Apply a rough heuristic to give a simple summary of an issue's review
1665 or CQ status, assuming adherence to a common workflow.
1666
1667 Returns None if no issue for this branch, or one of the following keywords:
1668 * 'error' - error from review tool (including deleted issues)
1669 * 'unsent' - not sent for review
1670 * 'waiting' - waiting for review
1671 * 'reply' - waiting for owner to reply to review
1672 * 'lgtm' - LGTM from at least one approved reviewer
1673 * 'commit' - in the commit queue
1674 * 'closed' - closed
1675 """
1676 if not self.GetIssue():
1677 return None
1678
1679 try:
1680 props = self.GetIssueProperties()
1681 except urllib2.HTTPError:
1682 return 'error'
1683
1684 if props.get('closed'):
1685 # Issue is closed.
1686 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001687 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001688 # Issue is in the commit queue.
1689 return 'commit'
1690
1691 try:
1692 reviewers = self.GetApprovingReviewers()
1693 except urllib2.HTTPError:
1694 return 'error'
1695
1696 if reviewers:
1697 # Was LGTM'ed.
1698 return 'lgtm'
1699
1700 messages = props.get('messages') or []
1701
tandrii9d2c7a32016-06-22 03:42:45 -07001702 # Skip CQ messages that don't require owner's action.
1703 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1704 if 'Dry run:' in messages[-1]['text']:
1705 messages.pop()
1706 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1707 # This message always follows prior messages from CQ,
1708 # so skip this too.
1709 messages.pop()
1710 else:
1711 # This is probably a CQ messages warranting user attention.
1712 break
1713
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001714 if not messages:
1715 # No message was sent.
1716 return 'unsent'
1717 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001718 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001719 return 'reply'
1720 return 'waiting'
1721
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001722 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001723 return self.RpcServer().update_description(
1724 self.GetIssue(), self.description)
1725
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001727 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001729 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001730 return self.SetFlags({flag: value})
1731
1732 def SetFlags(self, flags):
1733 """Sets flags on this CL/patchset in Rietveld.
1734
1735 The latest patchset in Rietveld must be the same as latest known locally.
1736 """
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001737 if not self.GetPatchset():
1738 DieWithError('The patchset needs to match. Send another patchset.')
1739 try:
tandrii4b233bd2016-07-06 03:50:29 -07001740 return self.RpcServer().set_flags(
1741 self.GetIssue(), self.GetPatchset(), flags)
vapierfd77ac72016-06-16 08:33:57 -07001742 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001743 if e.code == 404:
1744 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1745 if e.code == 403:
1746 DieWithError(
1747 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1748 'match?') % (self.GetIssue(), self.GetPatchset()))
1749 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001751 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001752 """Returns an upload.RpcServer() to access this review's rietveld instance.
1753 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001754 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001755 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001756 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001757 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001758 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001760 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001761 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001762 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 """Return the git setting that stores this change's most recent patchset."""
1766 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1767
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001768 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001770 branch = self.GetBranch()
1771 if branch:
1772 return 'branch.%s.rietveldserver' % branch
1773 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001774
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001775 def _PostUnsetIssueProperties(self):
1776 """Which branch-specific properties to erase when unsetting issue."""
1777 return ['rietveldserver']
1778
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001779 def GetRieveldObjForPresubmit(self):
1780 return self.RpcServer()
1781
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001782 def SetCQState(self, new_state):
1783 props = self.GetIssueProperties()
1784 if props.get('private'):
1785 DieWithError('Cannot set-commit on private issue')
1786
1787 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07001788 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001789 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07001790 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001791 else:
tandrii4b233bd2016-07-06 03:50:29 -07001792 assert new_state == _CQState.DRY_RUN
1793 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001794
1795
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001796 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1797 directory):
1798 # TODO(maruel): Use apply_issue.py
1799
1800 # PatchIssue should never be called with a dirty tree. It is up to the
1801 # caller to check this, but just in case we assert here since the
1802 # consequences of the caller not checking this could be dire.
1803 assert(not git_common.is_dirty_git_tree('apply'))
1804 assert(parsed_issue_arg.valid)
1805 self._changelist.issue = parsed_issue_arg.issue
1806 if parsed_issue_arg.hostname:
1807 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1808
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001809 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1810 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001811 assert parsed_issue_arg.patchset
1812 patchset = parsed_issue_arg.patchset
1813 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1814 else:
1815 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1816 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1817
1818 # Switch up to the top-level directory, if necessary, in preparation for
1819 # applying the patch.
1820 top = settings.GetRelativeRoot()
1821 if top:
1822 os.chdir(top)
1823
1824 # Git patches have a/ at the beginning of source paths. We strip that out
1825 # with a sed script rather than the -p flag to patch so we can feed either
1826 # Git or svn-style patches into the same apply command.
1827 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1828 try:
1829 patch_data = subprocess2.check_output(
1830 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1831 except subprocess2.CalledProcessError:
1832 DieWithError('Git patch mungling failed.')
1833 logging.info(patch_data)
1834
1835 # We use "git apply" to apply the patch instead of "patch" so that we can
1836 # pick up file adds.
1837 # The --index flag means: also insert into the index (so we catch adds).
1838 cmd = ['git', 'apply', '--index', '-p0']
1839 if directory:
1840 cmd.extend(('--directory', directory))
1841 if reject:
1842 cmd.append('--reject')
1843 elif IsGitVersionAtLeast('1.7.12'):
1844 cmd.append('--3way')
1845 try:
1846 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1847 stdin=patch_data, stdout=subprocess2.VOID)
1848 except subprocess2.CalledProcessError:
vapiera7fbd5a2016-06-16 09:17:49 -07001849 print('Failed to apply the patch')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001850 return 1
1851
1852 # If we had an issue, commit the current state and register the issue.
1853 if not nocommit:
1854 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1855 'patch from issue %(i)s at patchset '
1856 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1857 % {'i': self.GetIssue(), 'p': patchset})])
1858 self.SetIssue(self.GetIssue())
1859 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07001860 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001861 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001862 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001863 return 0
1864
1865 @staticmethod
1866 def ParseIssueURL(parsed_url):
1867 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1868 return None
wychen3c1c1722016-08-04 11:46:36 -07001869 # Rietveld patch: https://domain/<number>/#ps<patchset>
1870 match = re.match(r'/(\d+)/$', parsed_url.path)
1871 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
1872 if match and match2:
1873 return _RietveldParsedIssueNumberArgument(
1874 issue=int(match.group(1)),
1875 patchset=int(match2.group(1)),
1876 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001877 # Typical url: https://domain/<issue_number>[/[other]]
1878 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1879 if match:
1880 return _RietveldParsedIssueNumberArgument(
1881 issue=int(match.group(1)),
1882 hostname=parsed_url.netloc)
1883 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1884 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1885 if match:
1886 return _RietveldParsedIssueNumberArgument(
1887 issue=int(match.group(1)),
1888 patchset=int(match.group(2)),
1889 hostname=parsed_url.netloc,
1890 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1891 return None
1892
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001893 def CMDUploadChange(self, options, args, change):
1894 """Upload the patch to Rietveld."""
1895 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1896 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001897 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1898 if options.emulate_svn_auto_props:
1899 upload_args.append('--emulate_svn_auto_props')
1900
1901 change_desc = None
1902
1903 if options.email is not None:
1904 upload_args.extend(['--email', options.email])
1905
1906 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07001907 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001908 upload_args.extend(['--title', options.title])
1909 if options.message:
1910 upload_args.extend(['--message', options.message])
1911 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07001912 print('This branch is associated with issue %s. '
1913 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001914 else:
nodirca166002016-06-27 10:59:51 -07001915 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001916 upload_args.extend(['--title', options.title])
1917 message = (options.title or options.message or
1918 CreateDescriptionFromLog(args))
1919 change_desc = ChangeDescription(message)
1920 if options.reviewers or options.tbr_owners:
1921 change_desc.update_reviewers(options.reviewers,
1922 options.tbr_owners,
1923 change)
1924 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07001925 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001926
1927 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07001928 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001929 return 1
1930
1931 upload_args.extend(['--message', change_desc.description])
1932 if change_desc.get_reviewers():
1933 upload_args.append('--reviewers=%s' % ','.join(
1934 change_desc.get_reviewers()))
1935 if options.send_mail:
1936 if not change_desc.get_reviewers():
1937 DieWithError("Must specify reviewers to send email.")
1938 upload_args.append('--send_mail')
1939
1940 # We check this before applying rietveld.private assuming that in
1941 # rietveld.cc only addresses which we can send private CLs to are listed
1942 # if rietveld.private is set, and so we should ignore rietveld.cc only
1943 # when --private is specified explicitly on the command line.
1944 if options.private:
1945 logging.warn('rietveld.cc is ignored since private flag is specified. '
1946 'You need to review and add them manually if necessary.')
1947 cc = self.GetCCListWithoutDefault()
1948 else:
1949 cc = self.GetCCList()
1950 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1951 if cc:
1952 upload_args.extend(['--cc', cc])
1953
1954 if options.private or settings.GetDefaultPrivateFlag() == "True":
1955 upload_args.append('--private')
1956
1957 upload_args.extend(['--git_similarity', str(options.similarity)])
1958 if not options.find_copies:
1959 upload_args.extend(['--git_no_find_copies'])
1960
1961 # Include the upstream repo's URL in the change -- this is useful for
1962 # projects that have their source spread across multiple repos.
1963 remote_url = self.GetGitBaseUrlFromConfig()
1964 if not remote_url:
1965 if settings.GetIsGitSvn():
1966 remote_url = self.GetGitSvnRemoteUrl()
1967 else:
1968 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1969 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1970 self.GetUpstreamBranch().split('/')[-1])
1971 if remote_url:
1972 upload_args.extend(['--base_url', remote_url])
1973 remote, remote_branch = self.GetRemoteBranch()
1974 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1975 settings.GetPendingRefPrefix())
1976 if target_ref:
1977 upload_args.extend(['--target_ref', target_ref])
1978
1979 # Look for dependent patchsets. See crbug.com/480453 for more details.
1980 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1981 upstream_branch = ShortBranchName(upstream_branch)
1982 if remote is '.':
1983 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001984 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001985 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07001986 print()
1987 print('Skipping dependency patchset upload because git config '
1988 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1989 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001990 else:
1991 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001992 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001993 auth_config=auth_config)
1994 branch_cl_issue_url = branch_cl.GetIssueURL()
1995 branch_cl_issue = branch_cl.GetIssue()
1996 branch_cl_patchset = branch_cl.GetPatchset()
1997 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1998 upload_args.extend(
1999 ['--depends_on_patchset', '%s:%s' % (
2000 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002001 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002002 '\n'
2003 'The current branch (%s) is tracking a local branch (%s) with '
2004 'an associated CL.\n'
2005 'Adding %s/#ps%s as a dependency patchset.\n'
2006 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2007 branch_cl_patchset))
2008
2009 project = settings.GetProject()
2010 if project:
2011 upload_args.extend(['--project', project])
2012
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002013 try:
2014 upload_args = ['upload'] + upload_args + args
2015 logging.info('upload.RealMain(%s)', upload_args)
2016 issue, patchset = upload.RealMain(upload_args)
2017 issue = int(issue)
2018 patchset = int(patchset)
2019 except KeyboardInterrupt:
2020 sys.exit(1)
2021 except:
2022 # If we got an exception after the user typed a description for their
2023 # change, back up the description before re-raising.
2024 if change_desc:
2025 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2026 print('\nGot exception while uploading -- saving description to %s\n' %
2027 backup_path)
2028 backup_file = open(backup_path, 'w')
2029 backup_file.write(change_desc.description)
2030 backup_file.close()
2031 raise
2032
2033 if not self.GetIssue():
2034 self.SetIssue(issue)
2035 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002036 return 0
2037
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002038
2039class _GerritChangelistImpl(_ChangelistCodereviewBase):
2040 def __init__(self, changelist, auth_config=None):
2041 # auth_config is Rietveld thing, kept here to preserve interface only.
2042 super(_GerritChangelistImpl, self).__init__(changelist)
2043 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002044 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002045 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002046 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002047
2048 def _GetGerritHost(self):
2049 # Lazy load of configs.
2050 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002051 if self._gerrit_host and '.' not in self._gerrit_host:
2052 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2053 # This happens for internal stuff http://crbug.com/614312.
2054 parsed = urlparse.urlparse(self.GetRemoteUrl())
2055 if parsed.scheme == 'sso':
2056 print('WARNING: using non https URLs for remote is likely broken\n'
2057 ' Your current remote is: %s' % self.GetRemoteUrl())
2058 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2059 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002060 return self._gerrit_host
2061
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002062 def _GetGitHost(self):
2063 """Returns git host to be used when uploading change to Gerrit."""
2064 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2065
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002066 def GetCodereviewServer(self):
2067 if not self._gerrit_server:
2068 # If we're on a branch then get the server potentially associated
2069 # with that branch.
2070 if self.GetIssue():
2071 gerrit_server_setting = self.GetCodereviewServerSetting()
2072 if gerrit_server_setting:
2073 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2074 error_ok=True).strip()
2075 if self._gerrit_server:
2076 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2077 if not self._gerrit_server:
2078 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2079 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002080 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002081 parts[0] = parts[0] + '-review'
2082 self._gerrit_host = '.'.join(parts)
2083 self._gerrit_server = 'https://%s' % self._gerrit_host
2084 return self._gerrit_server
2085
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002086 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002087 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002088 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002089
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002090 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002091 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002092 if settings.GetGerritSkipEnsureAuthenticated():
2093 # For projects with unusual authentication schemes.
2094 # See http://crbug.com/603378.
2095 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002096 # Lazy-loader to identify Gerrit and Git hosts.
2097 if gerrit_util.GceAuthenticator.is_gce():
2098 return
2099 self.GetCodereviewServer()
2100 git_host = self._GetGitHost()
2101 assert self._gerrit_server and self._gerrit_host
2102 cookie_auth = gerrit_util.CookiesAuthenticator()
2103
2104 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2105 git_auth = cookie_auth.get_auth_header(git_host)
2106 if gerrit_auth and git_auth:
2107 if gerrit_auth == git_auth:
2108 return
2109 print((
2110 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2111 ' Check your %s or %s file for credentials of hosts:\n'
2112 ' %s\n'
2113 ' %s\n'
2114 ' %s') %
2115 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2116 git_host, self._gerrit_host,
2117 cookie_auth.get_new_password_message(git_host)))
2118 if not force:
2119 ask_for_data('If you know what you are doing, press Enter to continue, '
2120 'Ctrl+C to abort.')
2121 return
2122 else:
2123 missing = (
2124 [] if gerrit_auth else [self._gerrit_host] +
2125 [] if git_auth else [git_host])
2126 DieWithError('Credentials for the following hosts are required:\n'
2127 ' %s\n'
2128 'These are read from %s (or legacy %s)\n'
2129 '%s' % (
2130 '\n '.join(missing),
2131 cookie_auth.get_gitcookies_path(),
2132 cookie_auth.get_netrc_path(),
2133 cookie_auth.get_new_password_message(git_host)))
2134
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002135
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002136 def PatchsetSetting(self):
2137 """Return the git setting that stores this change's most recent patchset."""
2138 return 'branch.%s.gerritpatchset' % self.GetBranch()
2139
2140 def GetCodereviewServerSetting(self):
2141 """Returns the git setting that stores this change's Gerrit server."""
2142 branch = self.GetBranch()
2143 if branch:
2144 return 'branch.%s.gerritserver' % branch
2145 return None
2146
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002147 def _PostUnsetIssueProperties(self):
2148 """Which branch-specific properties to erase when unsetting issue."""
2149 return [
2150 'gerritserver',
2151 'gerritsquashhash',
2152 ]
2153
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002154 def GetRieveldObjForPresubmit(self):
2155 class ThisIsNotRietveldIssue(object):
2156 def __nonzero__(self):
2157 # This is a hack to make presubmit_support think that rietveld is not
2158 # defined, yet still ensure that calls directly result in a decent
2159 # exception message below.
2160 return False
2161
2162 def __getattr__(self, attr):
2163 print(
2164 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2165 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2166 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2167 'or use Rietveld for codereview.\n'
2168 'See also http://crbug.com/579160.' % attr)
2169 raise NotImplementedError()
2170 return ThisIsNotRietveldIssue()
2171
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002172 def GetGerritObjForPresubmit(self):
2173 return presubmit_support.GerritAccessor(self._GetGerritHost())
2174
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002175 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002176 """Apply a rough heuristic to give a simple summary of an issue's review
2177 or CQ status, assuming adherence to a common workflow.
2178
2179 Returns None if no issue for this branch, or one of the following keywords:
2180 * 'error' - error from review tool (including deleted issues)
2181 * 'unsent' - no reviewers added
2182 * 'waiting' - waiting for review
2183 * 'reply' - waiting for owner to reply to review
2184 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2185 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2186 * 'commit' - in the commit queue
2187 * 'closed' - abandoned
2188 """
2189 if not self.GetIssue():
2190 return None
2191
2192 try:
2193 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2194 except httplib.HTTPException:
2195 return 'error'
2196
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002197 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002198 return 'closed'
2199
2200 cq_label = data['labels'].get('Commit-Queue', {})
2201 if cq_label:
2202 # Vote value is a stringified integer, which we expect from 0 to 2.
2203 vote_value = cq_label.get('value', '0')
2204 vote_text = cq_label.get('values', {}).get(vote_value, '')
2205 if vote_text.lower() == 'commit':
2206 return 'commit'
2207
2208 lgtm_label = data['labels'].get('Code-Review', {})
2209 if lgtm_label:
2210 if 'rejected' in lgtm_label:
2211 return 'not lgtm'
2212 if 'approved' in lgtm_label:
2213 return 'lgtm'
2214
2215 if not data.get('reviewers', {}).get('REVIEWER', []):
2216 return 'unsent'
2217
2218 messages = data.get('messages', [])
2219 if messages:
2220 owner = data['owner'].get('_account_id')
2221 last_message_author = messages[-1].get('author', {}).get('_account_id')
2222 if owner != last_message_author:
2223 # Some reply from non-owner.
2224 return 'reply'
2225
2226 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002227
2228 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002229 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002230 return data['revisions'][data['current_revision']]['_number']
2231
2232 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002233 data = self._GetChangeDetail(['CURRENT_REVISION'])
2234 current_rev = data['current_revision']
2235 url = data['revisions'][current_rev]['fetch']['http']['url']
2236 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002237
2238 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002239 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2240 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002241
2242 def CloseIssue(self):
2243 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2244
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002245 def GetApprovingReviewers(self):
2246 """Returns a list of reviewers approving the change.
2247
2248 Note: not necessarily committers.
2249 """
2250 raise NotImplementedError()
2251
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002252 def SubmitIssue(self, wait_for_merge=True):
2253 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2254 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002255
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002256 def _GetChangeDetail(self, options=None, issue=None):
2257 options = options or []
2258 issue = issue or self.GetIssue()
2259 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002260 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2261 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002262
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002263 def CMDLand(self, force, bypass_hooks, verbose):
2264 if git_common.is_dirty_git_tree('land'):
2265 return 1
tandriid60367b2016-06-22 05:25:12 -07002266 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2267 if u'Commit-Queue' in detail.get('labels', {}):
2268 if not force:
2269 ask_for_data('\nIt seems this repository has a Commit Queue, '
2270 'which can test and land changes for you. '
2271 'Are you sure you wish to bypass it?\n'
2272 'Press Enter to continue, Ctrl+C to abort.')
2273
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002274 differs = True
2275 last_upload = RunGit(['config',
2276 'branch.%s.gerritsquashhash' % self.GetBranch()],
2277 error_ok=True).strip()
2278 # Note: git diff outputs nothing if there is no diff.
2279 if not last_upload or RunGit(['diff', last_upload]).strip():
2280 print('WARNING: some changes from local branch haven\'t been uploaded')
2281 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002282 if detail['current_revision'] == last_upload:
2283 differs = False
2284 else:
2285 print('WARNING: local branch contents differ from latest uploaded '
2286 'patchset')
2287 if differs:
2288 if not force:
2289 ask_for_data(
2290 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2291 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2292 elif not bypass_hooks:
2293 hook_results = self.RunHook(
2294 committing=True,
2295 may_prompt=not force,
2296 verbose=verbose,
2297 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2298 if not hook_results.should_continue():
2299 return 1
2300
2301 self.SubmitIssue(wait_for_merge=True)
2302 print('Issue %s has been submitted.' % self.GetIssueURL())
2303 return 0
2304
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002305 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2306 directory):
2307 assert not reject
2308 assert not nocommit
2309 assert not directory
2310 assert parsed_issue_arg.valid
2311
2312 self._changelist.issue = parsed_issue_arg.issue
2313
2314 if parsed_issue_arg.hostname:
2315 self._gerrit_host = parsed_issue_arg.hostname
2316 self._gerrit_server = 'https://%s' % self._gerrit_host
2317
2318 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2319
2320 if not parsed_issue_arg.patchset:
2321 # Use current revision by default.
2322 revision_info = detail['revisions'][detail['current_revision']]
2323 patchset = int(revision_info['_number'])
2324 else:
2325 patchset = parsed_issue_arg.patchset
2326 for revision_info in detail['revisions'].itervalues():
2327 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2328 break
2329 else:
2330 DieWithError('Couldn\'t find patchset %i in issue %i' %
2331 (parsed_issue_arg.patchset, self.GetIssue()))
2332
2333 fetch_info = revision_info['fetch']['http']
2334 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2335 RunGit(['cherry-pick', 'FETCH_HEAD'])
2336 self.SetIssue(self.GetIssue())
2337 self.SetPatchset(patchset)
2338 print('Committed patch for issue %i pathset %i locally' %
2339 (self.GetIssue(), self.GetPatchset()))
2340 return 0
2341
2342 @staticmethod
2343 def ParseIssueURL(parsed_url):
2344 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2345 return None
2346 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2347 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2348 # Short urls like https://domain/<issue_number> can be used, but don't allow
2349 # specifying the patchset (you'd 404), but we allow that here.
2350 if parsed_url.path == '/':
2351 part = parsed_url.fragment
2352 else:
2353 part = parsed_url.path
2354 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2355 if match:
2356 return _ParsedIssueNumberArgument(
2357 issue=int(match.group(2)),
2358 patchset=int(match.group(4)) if match.group(4) else None,
2359 hostname=parsed_url.netloc)
2360 return None
2361
tandrii16e0b4e2016-06-07 10:34:28 -07002362 def _GerritCommitMsgHookCheck(self, offer_removal):
2363 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2364 if not os.path.exists(hook):
2365 return
2366 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2367 # custom developer made one.
2368 data = gclient_utils.FileRead(hook)
2369 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2370 return
2371 print('Warning: you have Gerrit commit-msg hook installed.\n'
2372 'It is not neccessary for uploading with git cl in squash mode, '
2373 'and may interfere with it in subtle ways.\n'
2374 'We recommend you remove the commit-msg hook.')
2375 if offer_removal:
2376 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2377 if reply.lower().startswith('y'):
2378 gclient_utils.rm_file_or_tree(hook)
2379 print('Gerrit commit-msg hook removed.')
2380 else:
2381 print('OK, will keep Gerrit commit-msg hook in place.')
2382
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002383 def CMDUploadChange(self, options, args, change):
2384 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002385 if options.squash and options.no_squash:
2386 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002387
2388 if not options.squash and not options.no_squash:
2389 # Load default for user, repo, squash=true, in this order.
2390 options.squash = settings.GetSquashGerritUploads()
2391 elif options.no_squash:
2392 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002393
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002394 # We assume the remote called "origin" is the one we want.
2395 # It is probably not worthwhile to support different workflows.
2396 gerrit_remote = 'origin'
2397
2398 remote, remote_branch = self.GetRemoteBranch()
2399 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2400 pending_prefix='')
2401
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002402 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002403 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002404 if self.GetIssue():
2405 # Try to get the message from a previous upload.
2406 message = self.GetDescription()
2407 if not message:
2408 DieWithError(
2409 'failed to fetch description from current Gerrit issue %d\n'
2410 '%s' % (self.GetIssue(), self.GetIssueURL()))
2411 change_id = self._GetChangeDetail()['change_id']
2412 while True:
2413 footer_change_ids = git_footers.get_footer_change_id(message)
2414 if footer_change_ids == [change_id]:
2415 break
2416 if not footer_change_ids:
2417 message = git_footers.add_footer_change_id(message, change_id)
2418 print('WARNING: appended missing Change-Id to issue description')
2419 continue
2420 # There is already a valid footer but with different or several ids.
2421 # Doing this automatically is non-trivial as we don't want to lose
2422 # existing other footers, yet we want to append just 1 desired
2423 # Change-Id. Thus, just create a new footer, but let user verify the
2424 # new description.
2425 message = '%s\n\nChange-Id: %s' % (message, change_id)
2426 print(
2427 'WARNING: issue %s has Change-Id footer(s):\n'
2428 ' %s\n'
2429 'but issue has Change-Id %s, according to Gerrit.\n'
2430 'Please, check the proposed correction to the description, '
2431 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2432 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2433 change_id))
2434 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2435 if not options.force:
2436 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002437 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002438 message = change_desc.description
2439 if not message:
2440 DieWithError("Description is empty. Aborting...")
2441 # Continue the while loop.
2442 # Sanity check of this code - we should end up with proper message
2443 # footer.
2444 assert [change_id] == git_footers.get_footer_change_id(message)
2445 change_desc = ChangeDescription(message)
2446 else:
2447 change_desc = ChangeDescription(
2448 options.message or CreateDescriptionFromLog(args))
2449 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002450 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002451 if not change_desc.description:
2452 DieWithError("Description is empty. Aborting...")
2453 message = change_desc.description
2454 change_ids = git_footers.get_footer_change_id(message)
2455 if len(change_ids) > 1:
2456 DieWithError('too many Change-Id footers, at most 1 allowed.')
2457 if not change_ids:
2458 # Generate the Change-Id automatically.
2459 message = git_footers.add_footer_change_id(
2460 message, GenerateGerritChangeId(message))
2461 change_desc.set_description(message)
2462 change_ids = git_footers.get_footer_change_id(message)
2463 assert len(change_ids) == 1
2464 change_id = change_ids[0]
2465
2466 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2467 if remote is '.':
2468 # If our upstream branch is local, we base our squashed commit on its
2469 # squashed version.
2470 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2471 # Check the squashed hash of the parent.
2472 parent = RunGit(['config',
2473 'branch.%s.gerritsquashhash' % upstream_branch_name],
2474 error_ok=True).strip()
2475 # Verify that the upstream branch has been uploaded too, otherwise
2476 # Gerrit will create additional CLs when uploading.
2477 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2478 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002479 DieWithError(
2480 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002481 'Note: maybe you\'ve uploaded it with --no-squash. '
2482 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002483 ' git cl upload --squash\n' % upstream_branch_name)
2484 else:
2485 parent = self.GetCommonAncestorWithUpstream()
2486
2487 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2488 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2489 '-m', message]).strip()
2490 else:
2491 change_desc = ChangeDescription(
2492 options.message or CreateDescriptionFromLog(args))
2493 if not change_desc.description:
2494 DieWithError("Description is empty. Aborting...")
2495
2496 if not git_footers.get_footer_change_id(change_desc.description):
2497 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002498 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2499 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002500 ref_to_push = 'HEAD'
2501 parent = '%s/%s' % (gerrit_remote, branch)
2502 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2503
2504 assert change_desc
2505 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2506 ref_to_push)]).splitlines()
2507 if len(commits) > 1:
2508 print('WARNING: This will upload %d commits. Run the following command '
2509 'to see which commits will be uploaded: ' % len(commits))
2510 print('git log %s..%s' % (parent, ref_to_push))
2511 print('You can also use `git squash-branch` to squash these into a '
2512 'single commit.')
2513 ask_for_data('About to upload; enter to confirm.')
2514
2515 if options.reviewers or options.tbr_owners:
2516 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2517 change)
2518
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002519 # Extra options that can be specified at push time. Doc:
2520 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2521 refspec_opts = []
2522 if options.title:
2523 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2524 # reverse on its side.
2525 if '_' in options.title:
2526 print('WARNING: underscores in title will be converted to spaces.')
2527 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2528
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002529 if options.send_mail:
2530 if not change_desc.get_reviewers():
2531 DieWithError('Must specify reviewers to send email.')
2532 refspec_opts.append('notify=ALL')
2533 else:
2534 refspec_opts.append('notify=NONE')
2535
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002536 cc = self.GetCCList().split(',')
2537 if options.cc:
2538 cc.extend(options.cc)
2539 cc = filter(None, cc)
2540 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002541 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002542
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002543 if change_desc.get_reviewers():
2544 refspec_opts.extend('r=' + email.strip()
2545 for email in change_desc.get_reviewers())
2546
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002547 refspec_suffix = ''
2548 if refspec_opts:
2549 refspec_suffix = '%' + ','.join(refspec_opts)
2550 assert ' ' not in refspec_suffix, (
2551 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002552 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002553
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002554 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002555 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002556 print_stdout=True,
2557 # Flush after every line: useful for seeing progress when running as
2558 # recipe.
2559 filter_fn=lambda _: sys.stdout.flush())
2560
2561 if options.squash:
2562 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2563 change_numbers = [m.group(1)
2564 for m in map(regex.match, push_stdout.splitlines())
2565 if m]
2566 if len(change_numbers) != 1:
2567 DieWithError(
2568 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2569 'Change-Id: %s') % (len(change_numbers), change_id))
2570 self.SetIssue(change_numbers[0])
2571 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2572 ref_to_push])
2573 return 0
2574
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002575 def _AddChangeIdToCommitMessage(self, options, args):
2576 """Re-commits using the current message, assumes the commit hook is in
2577 place.
2578 """
2579 log_desc = options.message or CreateDescriptionFromLog(args)
2580 git_command = ['commit', '--amend', '-m', log_desc]
2581 RunGit(git_command)
2582 new_log_desc = CreateDescriptionFromLog(args)
2583 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002584 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002585 return new_log_desc
2586 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002587 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002588
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002589 def SetCQState(self, new_state):
2590 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002591 vote_map = {
2592 _CQState.NONE: 0,
2593 _CQState.DRY_RUN: 1,
2594 _CQState.COMMIT : 2,
2595 }
2596 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2597 labels={'Commit-Queue': vote_map[new_state]})
2598
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002599
2600_CODEREVIEW_IMPLEMENTATIONS = {
2601 'rietveld': _RietveldChangelistImpl,
2602 'gerrit': _GerritChangelistImpl,
2603}
2604
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002605
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002606def _add_codereview_select_options(parser):
2607 """Appends --gerrit and --rietveld options to force specific codereview."""
2608 parser.codereview_group = optparse.OptionGroup(
2609 parser, 'EXPERIMENTAL! Codereview override options')
2610 parser.add_option_group(parser.codereview_group)
2611 parser.codereview_group.add_option(
2612 '--gerrit', action='store_true',
2613 help='Force the use of Gerrit for codereview')
2614 parser.codereview_group.add_option(
2615 '--rietveld', action='store_true',
2616 help='Force the use of Rietveld for codereview')
2617
2618
2619def _process_codereview_select_options(parser, options):
2620 if options.gerrit and options.rietveld:
2621 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2622 options.forced_codereview = None
2623 if options.gerrit:
2624 options.forced_codereview = 'gerrit'
2625 elif options.rietveld:
2626 options.forced_codereview = 'rietveld'
2627
2628
tandriif9aefb72016-07-01 09:06:51 -07002629def _get_bug_line_values(default_project, bugs):
2630 """Given default_project and comma separated list of bugs, yields bug line
2631 values.
2632
2633 Each bug can be either:
2634 * a number, which is combined with default_project
2635 * string, which is left as is.
2636
2637 This function may produce more than one line, because bugdroid expects one
2638 project per line.
2639
2640 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2641 ['v8:123', 'chromium:789']
2642 """
2643 default_bugs = []
2644 others = []
2645 for bug in bugs.split(','):
2646 bug = bug.strip()
2647 if bug:
2648 try:
2649 default_bugs.append(int(bug))
2650 except ValueError:
2651 others.append(bug)
2652
2653 if default_bugs:
2654 default_bugs = ','.join(map(str, default_bugs))
2655 if default_project:
2656 yield '%s:%s' % (default_project, default_bugs)
2657 else:
2658 yield default_bugs
2659 for other in sorted(others):
2660 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2661 yield other
2662
2663
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002664class ChangeDescription(object):
2665 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002666 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002667 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002668
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002669 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002670 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002671
agable@chromium.org42c20792013-09-12 17:34:49 +00002672 @property # www.logilab.org/ticket/89786
2673 def description(self): # pylint: disable=E0202
2674 return '\n'.join(self._description_lines)
2675
2676 def set_description(self, desc):
2677 if isinstance(desc, basestring):
2678 lines = desc.splitlines()
2679 else:
2680 lines = [line.rstrip() for line in desc]
2681 while lines and not lines[0]:
2682 lines.pop(0)
2683 while lines and not lines[-1]:
2684 lines.pop(-1)
2685 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002686
piman@chromium.org336f9122014-09-04 02:16:55 +00002687 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002688 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002689 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002690 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002691 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002692 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002693
agable@chromium.org42c20792013-09-12 17:34:49 +00002694 # Get the set of R= and TBR= lines and remove them from the desciption.
2695 regexp = re.compile(self.R_LINE)
2696 matches = [regexp.match(line) for line in self._description_lines]
2697 new_desc = [l for i, l in enumerate(self._description_lines)
2698 if not matches[i]]
2699 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002700
agable@chromium.org42c20792013-09-12 17:34:49 +00002701 # Construct new unified R= and TBR= lines.
2702 r_names = []
2703 tbr_names = []
2704 for match in matches:
2705 if not match:
2706 continue
2707 people = cleanup_list([match.group(2).strip()])
2708 if match.group(1) == 'TBR':
2709 tbr_names.extend(people)
2710 else:
2711 r_names.extend(people)
2712 for name in r_names:
2713 if name not in reviewers:
2714 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002715 if add_owners_tbr:
2716 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07002717 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002718 all_reviewers = set(tbr_names + reviewers)
2719 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2720 all_reviewers)
2721 tbr_names.extend(owners_db.reviewers_for(missing_files,
2722 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002723 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2724 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2725
2726 # Put the new lines in the description where the old first R= line was.
2727 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2728 if 0 <= line_loc < len(self._description_lines):
2729 if new_tbr_line:
2730 self._description_lines.insert(line_loc, new_tbr_line)
2731 if new_r_line:
2732 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002733 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002734 if new_r_line:
2735 self.append_footer(new_r_line)
2736 if new_tbr_line:
2737 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002738
tandriif9aefb72016-07-01 09:06:51 -07002739 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002740 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002741 self.set_description([
2742 '# Enter a description of the change.',
2743 '# This will be displayed on the codereview site.',
2744 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002745 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002746 '--------------------',
2747 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002748
agable@chromium.org42c20792013-09-12 17:34:49 +00002749 regexp = re.compile(self.BUG_LINE)
2750 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002751 prefix = settings.GetBugPrefix()
2752 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
2753 for value in values:
2754 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
2755 self.append_footer('BUG=%s' % value)
2756
agable@chromium.org42c20792013-09-12 17:34:49 +00002757 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002758 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002759 if not content:
2760 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002761 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002762
2763 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002764 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2765 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002766 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002767 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002768
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002769 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002770 """Adds a footer line to the description.
2771
2772 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2773 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2774 that Gerrit footers are always at the end.
2775 """
2776 parsed_footer_line = git_footers.parse_footer(line)
2777 if parsed_footer_line:
2778 # Line is a gerrit footer in the form: Footer-Key: any value.
2779 # Thus, must be appended observing Gerrit footer rules.
2780 self.set_description(
2781 git_footers.add_footer(self.description,
2782 key=parsed_footer_line[0],
2783 value=parsed_footer_line[1]))
2784 return
2785
2786 if not self._description_lines:
2787 self._description_lines.append(line)
2788 return
2789
2790 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2791 if gerrit_footers:
2792 # git_footers.split_footers ensures that there is an empty line before
2793 # actual (gerrit) footers, if any. We have to keep it that way.
2794 assert top_lines and top_lines[-1] == ''
2795 top_lines, separator = top_lines[:-1], top_lines[-1:]
2796 else:
2797 separator = [] # No need for separator if there are no gerrit_footers.
2798
2799 prev_line = top_lines[-1] if top_lines else ''
2800 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2801 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2802 top_lines.append('')
2803 top_lines.append(line)
2804 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002805
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002806 def get_reviewers(self):
2807 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002808 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2809 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002810 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002811
2812
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002813def get_approving_reviewers(props):
2814 """Retrieves the reviewers that approved a CL from the issue properties with
2815 messages.
2816
2817 Note that the list may contain reviewers that are not committer, thus are not
2818 considered by the CQ.
2819 """
2820 return sorted(
2821 set(
2822 message['sender']
2823 for message in props['messages']
2824 if message['approval'] and message['sender'] in props['reviewers']
2825 )
2826 )
2827
2828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002829def FindCodereviewSettingsFile(filename='codereview.settings'):
2830 """Finds the given file starting in the cwd and going up.
2831
2832 Only looks up to the top of the repository unless an
2833 'inherit-review-settings-ok' file exists in the root of the repository.
2834 """
2835 inherit_ok_file = 'inherit-review-settings-ok'
2836 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002837 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002838 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2839 root = '/'
2840 while True:
2841 if filename in os.listdir(cwd):
2842 if os.path.isfile(os.path.join(cwd, filename)):
2843 return open(os.path.join(cwd, filename))
2844 if cwd == root:
2845 break
2846 cwd = os.path.dirname(cwd)
2847
2848
2849def LoadCodereviewSettingsFromFile(fileobj):
2850 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002851 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002853 def SetProperty(name, setting, unset_error_ok=False):
2854 fullname = 'rietveld.' + name
2855 if setting in keyvals:
2856 RunGit(['config', fullname, keyvals[setting]])
2857 else:
2858 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2859
2860 SetProperty('server', 'CODE_REVIEW_SERVER')
2861 # Only server setting is required. Other settings can be absent.
2862 # In that case, we ignore errors raised during option deletion attempt.
2863 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002864 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002865 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2866 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002867 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002868 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002869 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2870 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002871 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002872 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002873 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002874 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2875 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002876
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002877 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002878 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002879
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002880 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07002881 RunGit(['config', 'gerrit.squash-uploads',
2882 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002883
tandrii@chromium.org28253532016-04-14 13:46:56 +00002884 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002885 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002886 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2887
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002888 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2889 #should be of the form
2890 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2891 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2892 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2893 keyvals['ORIGIN_URL_CONFIG']])
2894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002895
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002896def urlretrieve(source, destination):
2897 """urllib is broken for SSL connections via a proxy therefore we
2898 can't use urllib.urlretrieve()."""
2899 with open(destination, 'w') as f:
2900 f.write(urllib2.urlopen(source).read())
2901
2902
ukai@chromium.org712d6102013-11-27 00:52:58 +00002903def hasSheBang(fname):
2904 """Checks fname is a #! script."""
2905 with open(fname) as f:
2906 return f.read(2).startswith('#!')
2907
2908
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002909# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2910def DownloadHooks(*args, **kwargs):
2911 pass
2912
2913
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002914def DownloadGerritHook(force):
2915 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002916
2917 Args:
2918 force: True to update hooks. False to install hooks if not present.
2919 """
2920 if not settings.GetIsGerrit():
2921 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002922 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002923 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2924 if not os.access(dst, os.X_OK):
2925 if os.path.exists(dst):
2926 if not force:
2927 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002928 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002929 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002930 if not hasSheBang(dst):
2931 DieWithError('Not a script: %s\n'
2932 'You need to download from\n%s\n'
2933 'into .git/hooks/commit-msg and '
2934 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002935 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2936 except Exception:
2937 if os.path.exists(dst):
2938 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002939 DieWithError('\nFailed to download hooks.\n'
2940 'You need to download from\n%s\n'
2941 'into .git/hooks/commit-msg and '
2942 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002943
2944
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002945
2946def GetRietveldCodereviewSettingsInteractively():
2947 """Prompt the user for settings."""
2948 server = settings.GetDefaultServerUrl(error_ok=True)
2949 prompt = 'Rietveld server (host[:port])'
2950 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2951 newserver = ask_for_data(prompt + ':')
2952 if not server and not newserver:
2953 newserver = DEFAULT_SERVER
2954 if newserver:
2955 newserver = gclient_utils.UpgradeToHttps(newserver)
2956 if newserver != server:
2957 RunGit(['config', 'rietveld.server', newserver])
2958
2959 def SetProperty(initial, caption, name, is_url):
2960 prompt = caption
2961 if initial:
2962 prompt += ' ("x" to clear) [%s]' % initial
2963 new_val = ask_for_data(prompt + ':')
2964 if new_val == 'x':
2965 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2966 elif new_val:
2967 if is_url:
2968 new_val = gclient_utils.UpgradeToHttps(new_val)
2969 if new_val != initial:
2970 RunGit(['config', 'rietveld.' + name, new_val])
2971
2972 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2973 SetProperty(settings.GetDefaultPrivateFlag(),
2974 'Private flag (rietveld only)', 'private', False)
2975 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2976 'tree-status-url', False)
2977 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2978 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2979 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2980 'run-post-upload-hook', False)
2981
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002982@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002983def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002984 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002985
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002986 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002987 'For Gerrit, see http://crbug.com/603116.')
2988 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002989 parser.add_option('--activate-update', action='store_true',
2990 help='activate auto-updating [rietveld] section in '
2991 '.git/config')
2992 parser.add_option('--deactivate-update', action='store_true',
2993 help='deactivate auto-updating [rietveld] section in '
2994 '.git/config')
2995 options, args = parser.parse_args(args)
2996
2997 if options.deactivate_update:
2998 RunGit(['config', 'rietveld.autoupdate', 'false'])
2999 return
3000
3001 if options.activate_update:
3002 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3003 return
3004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003005 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003006 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003007 return 0
3008
3009 url = args[0]
3010 if not url.endswith('codereview.settings'):
3011 url = os.path.join(url, 'codereview.settings')
3012
3013 # Load code review settings and download hooks (if available).
3014 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3015 return 0
3016
3017
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003018def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003019 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003020 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3021 branch = ShortBranchName(branchref)
3022 _, args = parser.parse_args(args)
3023 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003024 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003025 return RunGit(['config', 'branch.%s.base-url' % branch],
3026 error_ok=False).strip()
3027 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003028 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003029 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3030 error_ok=False).strip()
3031
3032
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003033def color_for_status(status):
3034 """Maps a Changelist status to color, for CMDstatus and other tools."""
3035 return {
3036 'unsent': Fore.RED,
3037 'waiting': Fore.BLUE,
3038 'reply': Fore.YELLOW,
3039 'lgtm': Fore.GREEN,
3040 'commit': Fore.MAGENTA,
3041 'closed': Fore.CYAN,
3042 'error': Fore.WHITE,
3043 }.get(status, Fore.WHITE)
3044
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003045
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003046def get_cl_statuses(changes, fine_grained, max_processes=None):
3047 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003048
3049 If fine_grained is true, this will fetch CL statuses from the server.
3050 Otherwise, simply indicate if there's a matching url for the given branches.
3051
3052 If max_processes is specified, it is used as the maximum number of processes
3053 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3054 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003055
3056 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003057 """
3058 # Silence upload.py otherwise it becomes unwieldly.
3059 upload.verbosity = 0
3060
3061 if fine_grained:
3062 # Process one branch synchronously to work through authentication, then
3063 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003064 if changes:
3065 fetch = lambda cl: (cl, cl.GetStatus())
3066 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003067
kmarshall3bff56b2016-06-06 18:31:47 -07003068 if not changes:
3069 # Exit early if there was only one branch to fetch.
3070 return
3071
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003072 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003073 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003074 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003075 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003076 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003077
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003078 fetched_cls = set()
3079 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003080 while True:
3081 try:
3082 row = it.next(timeout=5)
3083 except multiprocessing.TimeoutError:
3084 break
3085
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003086 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003087 yield row
3088
3089 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003090 for cl in set(changes_to_fetch) - fetched_cls:
3091 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003092
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003093 else:
3094 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003095 for cl in changes:
3096 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003097
rmistry@google.com2dd99862015-06-22 12:22:18 +00003098
3099def upload_branch_deps(cl, args):
3100 """Uploads CLs of local branches that are dependents of the current branch.
3101
3102 If the local branch dependency tree looks like:
3103 test1 -> test2.1 -> test3.1
3104 -> test3.2
3105 -> test2.2 -> test3.3
3106
3107 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3108 run on the dependent branches in this order:
3109 test2.1, test3.1, test3.2, test2.2, test3.3
3110
3111 Note: This function does not rebase your local dependent branches. Use it when
3112 you make a change to the parent branch that will not conflict with its
3113 dependent branches, and you would like their dependencies updated in
3114 Rietveld.
3115 """
3116 if git_common.is_dirty_git_tree('upload-branch-deps'):
3117 return 1
3118
3119 root_branch = cl.GetBranch()
3120 if root_branch is None:
3121 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3122 'Get on a branch!')
3123 if not cl.GetIssue() or not cl.GetPatchset():
3124 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3125 'patchset dependencies without an uploaded CL.')
3126
3127 branches = RunGit(['for-each-ref',
3128 '--format=%(refname:short) %(upstream:short)',
3129 'refs/heads'])
3130 if not branches:
3131 print('No local branches found.')
3132 return 0
3133
3134 # Create a dictionary of all local branches to the branches that are dependent
3135 # on it.
3136 tracked_to_dependents = collections.defaultdict(list)
3137 for b in branches.splitlines():
3138 tokens = b.split()
3139 if len(tokens) == 2:
3140 branch_name, tracked = tokens
3141 tracked_to_dependents[tracked].append(branch_name)
3142
vapiera7fbd5a2016-06-16 09:17:49 -07003143 print()
3144 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003145 dependents = []
3146 def traverse_dependents_preorder(branch, padding=''):
3147 dependents_to_process = tracked_to_dependents.get(branch, [])
3148 padding += ' '
3149 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003150 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003151 dependents.append(dependent)
3152 traverse_dependents_preorder(dependent, padding)
3153 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003154 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003155
3156 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003157 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003158 return 0
3159
vapiera7fbd5a2016-06-16 09:17:49 -07003160 print('This command will checkout all dependent branches and run '
3161 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003162 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3163
andybons@chromium.org962f9462016-02-03 20:00:42 +00003164 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003165 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003166 args.extend(['-t', 'Updated patchset dependency'])
3167
rmistry@google.com2dd99862015-06-22 12:22:18 +00003168 # Record all dependents that failed to upload.
3169 failures = {}
3170 # Go through all dependents, checkout the branch and upload.
3171 try:
3172 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003173 print()
3174 print('--------------------------------------')
3175 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003176 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003177 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003178 try:
3179 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003180 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003181 failures[dependent_branch] = 1
3182 except: # pylint: disable=W0702
3183 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003184 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003185 finally:
3186 # Swap back to the original root branch.
3187 RunGit(['checkout', '-q', root_branch])
3188
vapiera7fbd5a2016-06-16 09:17:49 -07003189 print()
3190 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003191 for dependent_branch in dependents:
3192 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003193 print(' %s : %s' % (dependent_branch, upload_status))
3194 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003195
3196 return 0
3197
3198
kmarshall3bff56b2016-06-06 18:31:47 -07003199def CMDarchive(parser, args):
3200 """Archives and deletes branches associated with closed changelists."""
3201 parser.add_option(
3202 '-j', '--maxjobs', action='store', type=int,
3203 help='The maximum number of jobs to use when retrieving review status')
3204 parser.add_option(
3205 '-f', '--force', action='store_true',
3206 help='Bypasses the confirmation prompt.')
3207
3208 auth.add_auth_options(parser)
3209 options, args = parser.parse_args(args)
3210 if args:
3211 parser.error('Unsupported args: %s' % ' '.join(args))
3212 auth_config = auth.extract_auth_config_from_options(options)
3213
3214 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3215 if not branches:
3216 return 0
3217
vapiera7fbd5a2016-06-16 09:17:49 -07003218 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003219 changes = [Changelist(branchref=b, auth_config=auth_config)
3220 for b in branches.splitlines()]
3221 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3222 statuses = get_cl_statuses(changes,
3223 fine_grained=True,
3224 max_processes=options.maxjobs)
3225 proposal = [(cl.GetBranch(),
3226 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3227 for cl, status in statuses
3228 if status == 'closed']
3229 proposal.sort()
3230
3231 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003232 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003233 return 0
3234
3235 current_branch = GetCurrentBranch()
3236
vapiera7fbd5a2016-06-16 09:17:49 -07003237 print('\nBranches with closed issues that will be archived:\n')
3238 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
kmarshall3bff56b2016-06-06 18:31:47 -07003239 for next_item in proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003240 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003241
3242 if any(branch == current_branch for branch, _ in proposal):
3243 print('You are currently on a branch \'%s\' which is associated with a '
3244 'closed codereview issue, so archive cannot proceed. Please '
3245 'checkout another branch and run this command again.' %
3246 current_branch)
3247 return 1
3248
3249 if not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003250 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3251 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003252 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003253 return 1
3254
3255 for branch, tagname in proposal:
3256 RunGit(['tag', tagname, branch])
3257 RunGit(['branch', '-D', branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003258 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003259
3260 return 0
3261
3262
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003263def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003264 """Show status of changelists.
3265
3266 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003267 - Red not sent for review or broken
3268 - Blue waiting for review
3269 - Yellow waiting for you to reply to review
3270 - Green LGTM'ed
3271 - Magenta in the commit queue
3272 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003273
3274 Also see 'git cl comments'.
3275 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003276 parser.add_option('--field',
3277 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003278 parser.add_option('-f', '--fast', action='store_true',
3279 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003280 parser.add_option(
3281 '-j', '--maxjobs', action='store', type=int,
3282 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003283
3284 auth.add_auth_options(parser)
3285 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003286 if args:
3287 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003288 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003290 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003291 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003292 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003293 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003294 elif options.field == 'id':
3295 issueid = cl.GetIssue()
3296 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003297 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003298 elif options.field == 'patch':
3299 patchset = cl.GetPatchset()
3300 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003301 print(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003302 elif options.field == 'url':
3303 url = cl.GetIssueURL()
3304 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003305 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003306 return 0
3307
3308 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3309 if not branches:
3310 print('No local branch found.')
3311 return 0
3312
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003313 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003314 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003315 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003316 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003317 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003318 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003319 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003320
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003321 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003322 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3323 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3324 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003325 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003326 c, status = output.next()
3327 branch_statuses[c.GetBranch()] = status
3328 status = branch_statuses.pop(branch)
3329 url = cl.GetIssueURL()
3330 if url and (not status or status == 'error'):
3331 # The issue probably doesn't exist anymore.
3332 url += ' (broken)'
3333
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003334 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003335 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003336 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003337 color = ''
3338 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003339 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003340 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003341 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003342 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003343
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003344 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003345 print()
3346 print('Current branch:',)
3347 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003348 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003349 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003350 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003351 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003352 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003353 print('Issue description:')
3354 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003355 return 0
3356
3357
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003358def colorize_CMDstatus_doc():
3359 """To be called once in main() to add colors to git cl status help."""
3360 colors = [i for i in dir(Fore) if i[0].isupper()]
3361
3362 def colorize_line(line):
3363 for color in colors:
3364 if color in line.upper():
3365 # Extract whitespaces first and the leading '-'.
3366 indent = len(line) - len(line.lstrip(' ')) + 1
3367 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3368 return line
3369
3370 lines = CMDstatus.__doc__.splitlines()
3371 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3372
3373
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003374@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003375def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003376 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003377
3378 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003379 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003380 parser.add_option('-r', '--reverse', action='store_true',
3381 help='Lookup the branch(es) for the specified issues. If '
3382 'no issues are specified, all branches with mapped '
3383 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003384 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003385 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003386 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003387
dnj@chromium.org406c4402015-03-03 17:22:28 +00003388 if options.reverse:
3389 branches = RunGit(['for-each-ref', 'refs/heads',
3390 '--format=%(refname:short)']).splitlines()
3391
3392 # Reverse issue lookup.
3393 issue_branch_map = {}
3394 for branch in branches:
3395 cl = Changelist(branchref=branch)
3396 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3397 if not args:
3398 args = sorted(issue_branch_map.iterkeys())
3399 for issue in args:
3400 if not issue:
3401 continue
vapiera7fbd5a2016-06-16 09:17:49 -07003402 print('Branch for issue number %s: %s' % (
3403 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
dnj@chromium.org406c4402015-03-03 17:22:28 +00003404 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003405 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003406 if len(args) > 0:
3407 try:
3408 issue = int(args[0])
3409 except ValueError:
3410 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003411 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003412 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003413 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003414 return 0
3415
3416
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003417def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003418 """Shows or posts review comments for any changelist."""
3419 parser.add_option('-a', '--add-comment', dest='comment',
3420 help='comment to add to an issue')
3421 parser.add_option('-i', dest='issue',
3422 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003423 parser.add_option('-j', '--json-file',
3424 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003425 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003426 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003427 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003428
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003429 issue = None
3430 if options.issue:
3431 try:
3432 issue = int(options.issue)
3433 except ValueError:
3434 DieWithError('A review issue id is expected to be a number')
3435
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003436 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003437
3438 if options.comment:
3439 cl.AddComment(options.comment)
3440 return 0
3441
3442 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003443 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003444 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003445 summary.append({
3446 'date': message['date'],
3447 'lgtm': False,
3448 'message': message['text'],
3449 'not_lgtm': False,
3450 'sender': message['sender'],
3451 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003452 if message['disapproval']:
3453 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003454 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003455 elif message['approval']:
3456 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003457 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003458 elif message['sender'] == data['owner_email']:
3459 color = Fore.MAGENTA
3460 else:
3461 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003462 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003463 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003464 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003465 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003466 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003467 if options.json_file:
3468 with open(options.json_file, 'wb') as f:
3469 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003470 return 0
3471
3472
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003473@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003474def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003475 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003476 parser.add_option('-d', '--display', action='store_true',
3477 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003478 parser.add_option('-n', '--new-description',
3479 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003480
3481 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003482 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003483 options, args = parser.parse_args(args)
3484 _process_codereview_select_options(parser, options)
3485
3486 target_issue = None
3487 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003488 target_issue = ParseIssueNumberArgument(args[0])
3489 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003490 parser.print_help()
3491 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003492
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003493 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003494
martiniss6eda05f2016-06-30 10:18:35 -07003495 kwargs = {
3496 'auth_config': auth_config,
3497 'codereview': options.forced_codereview,
3498 }
3499 if target_issue:
3500 kwargs['issue'] = target_issue.issue
3501 if options.forced_codereview == 'rietveld':
3502 kwargs['rietveld_server'] = target_issue.hostname
3503
3504 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003505
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003506 if not cl.GetIssue():
3507 DieWithError('This branch has no associated changelist.')
3508 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003509
smut@google.com34fb6b12015-07-13 20:03:26 +00003510 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003511 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003512 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003513
3514 if options.new_description:
3515 text = options.new_description
3516 if text == '-':
3517 text = '\n'.join(l.rstrip() for l in sys.stdin)
3518
3519 description.set_description(text)
3520 else:
3521 description.prompt()
3522
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003523 if cl.GetDescription() != description.description:
3524 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003525 return 0
3526
3527
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003528def CreateDescriptionFromLog(args):
3529 """Pulls out the commit log to use as a base for the CL description."""
3530 log_args = []
3531 if len(args) == 1 and not args[0].endswith('.'):
3532 log_args = [args[0] + '..']
3533 elif len(args) == 1 and args[0].endswith('...'):
3534 log_args = [args[0][:-1]]
3535 elif len(args) == 2:
3536 log_args = [args[0] + '..' + args[1]]
3537 else:
3538 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003539 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003540
3541
thestig@chromium.org44202a22014-03-11 19:22:18 +00003542def CMDlint(parser, args):
3543 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003544 parser.add_option('--filter', action='append', metavar='-x,+y',
3545 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003546 auth.add_auth_options(parser)
3547 options, args = parser.parse_args(args)
3548 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003549
3550 # Access to a protected member _XX of a client class
3551 # pylint: disable=W0212
3552 try:
3553 import cpplint
3554 import cpplint_chromium
3555 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003556 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003557 return 1
3558
3559 # Change the current working directory before calling lint so that it
3560 # shows the correct base.
3561 previous_cwd = os.getcwd()
3562 os.chdir(settings.GetRoot())
3563 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003564 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003565 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3566 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003567 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003568 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003569 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003570
3571 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003572 command = args + files
3573 if options.filter:
3574 command = ['--filter=' + ','.join(options.filter)] + command
3575 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003576
3577 white_regex = re.compile(settings.GetLintRegex())
3578 black_regex = re.compile(settings.GetLintIgnoreRegex())
3579 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3580 for filename in filenames:
3581 if white_regex.match(filename):
3582 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003583 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003584 else:
3585 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3586 extra_check_functions)
3587 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003588 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003589 finally:
3590 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003591 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003592 if cpplint._cpplint_state.error_count != 0:
3593 return 1
3594 return 0
3595
3596
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003597def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003598 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003599 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003600 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003601 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003602 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003603 auth.add_auth_options(parser)
3604 options, args = parser.parse_args(args)
3605 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003606
sbc@chromium.org71437c02015-04-09 19:29:40 +00003607 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07003608 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003609 return 1
3610
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612 if args:
3613 base_branch = args[0]
3614 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003615 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003616 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003617
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003618 cl.RunHook(
3619 committing=not options.upload,
3620 may_prompt=False,
3621 verbose=options.verbose,
3622 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003623 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624
3625
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003626def GenerateGerritChangeId(message):
3627 """Returns Ixxxxxx...xxx change id.
3628
3629 Works the same way as
3630 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3631 but can be called on demand on all platforms.
3632
3633 The basic idea is to generate git hash of a state of the tree, original commit
3634 message, author/committer info and timestamps.
3635 """
3636 lines = []
3637 tree_hash = RunGitSilent(['write-tree'])
3638 lines.append('tree %s' % tree_hash.strip())
3639 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3640 if code == 0:
3641 lines.append('parent %s' % parent.strip())
3642 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3643 lines.append('author %s' % author.strip())
3644 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3645 lines.append('committer %s' % committer.strip())
3646 lines.append('')
3647 # Note: Gerrit's commit-hook actually cleans message of some lines and
3648 # whitespace. This code is not doing this, but it clearly won't decrease
3649 # entropy.
3650 lines.append(message)
3651 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3652 stdin='\n'.join(lines))
3653 return 'I%s' % change_hash.strip()
3654
3655
wittman@chromium.org455dc922015-01-26 20:15:50 +00003656def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3657 """Computes the remote branch ref to use for the CL.
3658
3659 Args:
3660 remote (str): The git remote for the CL.
3661 remote_branch (str): The git remote branch for the CL.
3662 target_branch (str): The target branch specified by the user.
3663 pending_prefix (str): The pending prefix from the settings.
3664 """
3665 if not (remote and remote_branch):
3666 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003667
wittman@chromium.org455dc922015-01-26 20:15:50 +00003668 if target_branch:
3669 # Cannonicalize branch references to the equivalent local full symbolic
3670 # refs, which are then translated into the remote full symbolic refs
3671 # below.
3672 if '/' not in target_branch:
3673 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3674 else:
3675 prefix_replacements = (
3676 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3677 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3678 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3679 )
3680 match = None
3681 for regex, replacement in prefix_replacements:
3682 match = re.search(regex, target_branch)
3683 if match:
3684 remote_branch = target_branch.replace(match.group(0), replacement)
3685 break
3686 if not match:
3687 # This is a branch path but not one we recognize; use as-is.
3688 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003689 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3690 # Handle the refs that need to land in different refs.
3691 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003692
wittman@chromium.org455dc922015-01-26 20:15:50 +00003693 # Create the true path to the remote branch.
3694 # Does the following translation:
3695 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3696 # * refs/remotes/origin/master -> refs/heads/master
3697 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3698 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3699 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3700 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3701 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3702 'refs/heads/')
3703 elif remote_branch.startswith('refs/remotes/branch-heads'):
3704 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3705 # If a pending prefix exists then replace refs/ with it.
3706 if pending_prefix:
3707 remote_branch = remote_branch.replace('refs/', pending_prefix)
3708 return remote_branch
3709
3710
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003711def cleanup_list(l):
3712 """Fixes a list so that comma separated items are put as individual items.
3713
3714 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3715 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3716 """
3717 items = sum((i.split(',') for i in l), [])
3718 stripped_items = (i.strip() for i in items)
3719 return sorted(filter(None, stripped_items))
3720
3721
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003722@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003723def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003724 """Uploads the current changelist to codereview.
3725
3726 Can skip dependency patchset uploads for a branch by running:
3727 git config branch.branch_name.skip-deps-uploads True
3728 To unset run:
3729 git config --unset branch.branch_name.skip-deps-uploads
3730 Can also set the above globally by using the --global flag.
3731 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003732 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3733 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003734 parser.add_option('--bypass-watchlists', action='store_true',
3735 dest='bypass_watchlists',
3736 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003737 parser.add_option('-f', action='store_true', dest='force',
3738 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003739 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07003740 parser.add_option('-b', '--bug',
3741 help='pre-populate the bug number(s) for this issue. '
3742 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07003743 parser.add_option('--message-file', dest='message_file',
3744 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003745 parser.add_option('-t', dest='title',
3746 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003747 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003748 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003749 help='reviewer email addresses')
3750 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003751 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003752 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003753 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003754 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003755 parser.add_option('--emulate_svn_auto_props',
3756 '--emulate-svn-auto-props',
3757 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003758 dest="emulate_svn_auto_props",
3759 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003760 parser.add_option('-c', '--use-commit-queue', action='store_true',
3761 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003762 parser.add_option('--private', action='store_true',
3763 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003764 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003765 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003766 metavar='TARGET',
3767 help='Apply CL to remote ref TARGET. ' +
3768 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003769 parser.add_option('--squash', action='store_true',
3770 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003771 parser.add_option('--no-squash', action='store_true',
3772 help='Don\'t squash multiple commits into one ' +
3773 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003774 parser.add_option('--email', default=None,
3775 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003776 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3777 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003778 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3779 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003780 help='Send the patchset to do a CQ dry run right after '
3781 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003782 parser.add_option('--dependencies', action='store_true',
3783 help='Uploads CLs of all the local branches that depend on '
3784 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003785
rmistry@google.com2dd99862015-06-22 12:22:18 +00003786 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003787 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003788 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003789 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003790 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003791 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003792 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003793
sbc@chromium.org71437c02015-04-09 19:29:40 +00003794 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003795 return 1
3796
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003797 options.reviewers = cleanup_list(options.reviewers)
3798 options.cc = cleanup_list(options.cc)
3799
tandriib80458a2016-06-23 12:20:07 -07003800 if options.message_file:
3801 if options.message:
3802 parser.error('only one of --message and --message-file allowed.')
3803 options.message = gclient_utils.FileRead(options.message_file)
3804 options.message_file = None
3805
tandrii4d0545a2016-07-06 03:56:49 -07003806 if options.cq_dry_run and options.use_commit_queue:
3807 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
3808
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003809 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3810 settings.GetIsGerrit()
3811
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003812 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003813 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003814
3815
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003816def IsSubmoduleMergeCommit(ref):
3817 # When submodules are added to the repo, we expect there to be a single
3818 # non-git-svn merge commit at remote HEAD with a signature comment.
3819 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003820 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003821 return RunGit(cmd) != ''
3822
3823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003825 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003826
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003827 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3828 upstream and closes the issue automatically and atomically.
3829
3830 Otherwise (in case of Rietveld):
3831 Squashes branch into a single commit.
3832 Updates changelog with metadata (e.g. pointer to review).
3833 Pushes/dcommits the code upstream.
3834 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003835 """
3836 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3837 help='bypass upload presubmit hook')
3838 parser.add_option('-m', dest='message',
3839 help="override review description")
3840 parser.add_option('-f', action='store_true', dest='force',
3841 help="force yes to questions (don't prompt)")
3842 parser.add_option('-c', dest='contributor',
3843 help="external contributor for patch (appended to " +
3844 "description and used as author for git). Should be " +
3845 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003846 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003847 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003848 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003849 auth_config = auth.extract_auth_config_from_options(options)
3850
3851 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003852
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003853 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3854 if cl.IsGerrit():
3855 if options.message:
3856 # This could be implemented, but it requires sending a new patch to
3857 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3858 # Besides, Gerrit has the ability to change the commit message on submit
3859 # automatically, thus there is no need to support this option (so far?).
3860 parser.error('-m MESSAGE option is not supported for Gerrit.')
3861 if options.contributor:
3862 parser.error(
3863 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3864 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3865 'the contributor\'s "name <email>". If you can\'t upload such a '
3866 'commit for review, contact your repository admin and request'
3867 '"Forge-Author" permission.')
3868 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3869 options.verbose)
3870
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003871 current = cl.GetBranch()
3872 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3873 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07003874 print()
3875 print('Attempting to push branch %r into another local branch!' % current)
3876 print()
3877 print('Either reparent this branch on top of origin/master:')
3878 print(' git reparent-branch --root')
3879 print()
3880 print('OR run `git rebase-update` if you think the parent branch is ')
3881 print('already committed.')
3882 print()
3883 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003884 return 1
3885
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003886 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003887 # Default to merging against our best guess of the upstream branch.
3888 args = [cl.GetUpstreamBranch()]
3889
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003890 if options.contributor:
3891 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07003892 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003893 return 1
3894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003895 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003896 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003897
sbc@chromium.org71437c02015-04-09 19:29:40 +00003898 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003899 return 1
3900
3901 # This rev-list syntax means "show all commits not in my branch that
3902 # are in base_branch".
3903 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3904 base_branch]).splitlines()
3905 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003906 print('Base branch "%s" has %d commits '
3907 'not in this branch.' % (base_branch, len(upstream_commits)))
3908 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003909 return 1
3910
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003911 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003912 svn_head = None
3913 if cmd == 'dcommit' or base_has_submodules:
3914 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3915 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003916
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003918 # If the base_head is a submodule merge commit, the first parent of the
3919 # base_head should be a git-svn commit, which is what we're interested in.
3920 base_svn_head = base_branch
3921 if base_has_submodules:
3922 base_svn_head += '^1'
3923
3924 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003925 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07003926 print('This branch has %d additional commits not upstreamed yet.'
3927 % len(extra_commits.splitlines()))
3928 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
3929 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003930 return 1
3931
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003932 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003933 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003934 author = None
3935 if options.contributor:
3936 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003937 hook_results = cl.RunHook(
3938 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003939 may_prompt=not options.force,
3940 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003941 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003942 if not hook_results.should_continue():
3943 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003944
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003945 # Check the tree status if the tree status URL is set.
3946 status = GetTreeStatus()
3947 if 'closed' == status:
3948 print('The tree is closed. Please wait for it to reopen. Use '
3949 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3950 return 1
3951 elif 'unknown' == status:
3952 print('Unable to determine tree status. Please verify manually and '
3953 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3954 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003955
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003956 change_desc = ChangeDescription(options.message)
3957 if not change_desc.description and cl.GetIssue():
3958 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003959
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003960 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003961 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003962 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003963 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003964 print('No description set.')
3965 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00003966 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003967
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003968 # Keep a separate copy for the commit message, because the commit message
3969 # contains the link to the Rietveld issue, while the Rietveld message contains
3970 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003971 # Keep a separate copy for the commit message.
3972 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003973 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003974
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003975 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003976 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003977 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003978 # after it. Add a period on a new line to circumvent this. Also add a space
3979 # before the period to make sure that Gitiles continues to correctly resolve
3980 # the URL.
3981 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003982 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003983 commit_desc.append_footer('Patch from %s.' % options.contributor)
3984
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003985 print('Description:')
3986 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003988 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003989 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003990 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003991
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003992 # We want to squash all this branch's commits into one commit with the proper
3993 # description. We do this by doing a "reset --soft" to the base branch (which
3994 # keeps the working copy the same), then dcommitting that. If origin/master
3995 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3996 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003998 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3999 # Delete the branches if they exist.
4000 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4001 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4002 result = RunGitWithCode(showref_cmd)
4003 if result[0] == 0:
4004 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005
4006 # We might be in a directory that's present in this branch but not in the
4007 # trunk. Move up to the top of the tree so that git commands that expect a
4008 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004009 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010 if rel_base_path:
4011 os.chdir(rel_base_path)
4012
4013 # Stuff our change into the merge branch.
4014 # We wrap in a try...finally block so if anything goes wrong,
4015 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004016 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004017 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004018 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004019 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004020 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004021 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004022 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004023 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004024 RunGit(
4025 [
4026 'commit', '--author', options.contributor,
4027 '-m', commit_desc.description,
4028 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004029 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004030 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004031 if base_has_submodules:
4032 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4033 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4034 RunGit(['checkout', CHERRY_PICK_BRANCH])
4035 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004036 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004037 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004038 mirror = settings.GetGitMirror(remote)
4039 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004040 pending_prefix = settings.GetPendingRefPrefix()
4041 if not pending_prefix or branch.startswith(pending_prefix):
4042 # If not using refs/pending/heads/* at all, or target ref is already set
4043 # to pending, then push to the target ref directly.
4044 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004045 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004046 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004047 else:
4048 # Cherry-pick the change on top of pending ref and then push it.
4049 assert branch.startswith('refs/'), branch
4050 assert pending_prefix[-1] == '/', pending_prefix
4051 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004052 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004053 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004054 if retcode == 0:
4055 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004056 else:
4057 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004058 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004059 'svn', 'dcommit',
4060 '-C%s' % options.similarity,
4061 '--no-rebase', '--rmdir',
4062 ]
4063 if settings.GetForceHttpsCommitUrl():
4064 # Allow forcing https commit URLs for some projects that don't allow
4065 # committing to http URLs (like Google Code).
4066 remote_url = cl.GetGitSvnRemoteUrl()
4067 if urlparse.urlparse(remote_url).scheme == 'http':
4068 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004069 cmd_args.append('--commit-url=%s' % remote_url)
4070 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004071 if 'Committed r' in output:
4072 revision = re.match(
4073 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4074 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075 finally:
4076 # And then swap back to the original branch and clean up.
4077 RunGit(['checkout', '-q', cl.GetBranch()])
4078 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004079 if base_has_submodules:
4080 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004081
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004082 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004083 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004084 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004085
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004086 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004087 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004088 try:
4089 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4090 # We set pushed_to_pending to False, since it made it all the way to the
4091 # real ref.
4092 pushed_to_pending = False
4093 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004094 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004096 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004097 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004098 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004099 if not to_pending:
4100 if viewvc_url and revision:
4101 change_desc.append_footer(
4102 'Committed: %s%s' % (viewvc_url, revision))
4103 elif revision:
4104 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004105 print('Closing issue '
4106 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004107 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004109 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004110 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004111 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004112 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004113 if options.bypass_hooks:
4114 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4115 else:
4116 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004117 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004118
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004119 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004120 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004121 print('The commit is in the pending queue (%s).' % pending_ref)
4122 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4123 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004124
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004125 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4126 if os.path.isfile(hook):
4127 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004128
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004129 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004130
4131
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004132def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004133 print()
4134 print('Waiting for commit to be landed on %s...' % real_ref)
4135 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004136 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4137 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004138 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004139
4140 loop = 0
4141 while True:
4142 sys.stdout.write('fetching (%d)... \r' % loop)
4143 sys.stdout.flush()
4144 loop += 1
4145
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004146 if mirror:
4147 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004148 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4149 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4150 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4151 for commit in commits.splitlines():
4152 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004153 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004154 return commit
4155
4156 current_rev = to_rev
4157
4158
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004159def PushToGitPending(remote, pending_ref, upstream_ref):
4160 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4161
4162 Returns:
4163 (retcode of last operation, output log of last operation).
4164 """
4165 assert pending_ref.startswith('refs/'), pending_ref
4166 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4167 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4168 code = 0
4169 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004170 max_attempts = 3
4171 attempts_left = max_attempts
4172 while attempts_left:
4173 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004174 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004175 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004176
4177 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004178 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004179 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004180 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004181 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004182 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004183 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004184 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004185 continue
4186
4187 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004189 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004190 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004191 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004192 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4193 'the following files have merge conflicts:' % pending_ref)
4194 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4195 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004196 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004197 return code, out
4198
4199 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004200 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004201 code, out = RunGitWithCode(
4202 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4203 if code == 0:
4204 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004205 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004206 return code, out
4207
vapiera7fbd5a2016-06-16 09:17:49 -07004208 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004209 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004210 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004211 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004212 print('Fatal push error. Make sure your .netrc credentials and git '
4213 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004214 return code, out
4215
vapiera7fbd5a2016-06-16 09:17:49 -07004216 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004217 return code, out
4218
4219
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004220def IsFatalPushFailure(push_stdout):
4221 """True if retrying push won't help."""
4222 return '(prohibited by Gerrit)' in push_stdout
4223
4224
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004225@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004226def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004227 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004229 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004230 # If it looks like previous commits were mirrored with git-svn.
4231 message = """This repository appears to be a git-svn mirror, but no
4232upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4233 else:
4234 message = """This doesn't appear to be an SVN repository.
4235If your project has a true, writeable git repository, you probably want to run
4236'git cl land' instead.
4237If your project has a git mirror of an upstream SVN master, you probably need
4238to run 'git svn init'.
4239
4240Using the wrong command might cause your commit to appear to succeed, and the
4241review to be closed, without actually landing upstream. If you choose to
4242proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004243 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004244 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004245 # TODO(tandrii): kill this post SVN migration with
4246 # https://codereview.chromium.org/2076683002
4247 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4248 'Please let us know of this project you are committing to:'
4249 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250 return SendUpstream(parser, args, 'dcommit')
4251
4252
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004253@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004254def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004255 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004256 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004257 print('This appears to be an SVN repository.')
4258 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004259 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004260 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004261 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004262
4263
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004264@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004265def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004266 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004267 parser.add_option('-b', dest='newbranch',
4268 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004269 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004270 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004271 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4272 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004273 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004274 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004275 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004276 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004277 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004278 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004279
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004280
4281 group = optparse.OptionGroup(
4282 parser,
4283 'Options for continuing work on the current issue uploaded from a '
4284 'different clone (e.g. different machine). Must be used independently '
4285 'from the other options. No issue number should be specified, and the '
4286 'branch must have an issue number associated with it')
4287 group.add_option('--reapply', action='store_true', dest='reapply',
4288 help='Reset the branch and reapply the issue.\n'
4289 'CAUTION: This will undo any local changes in this '
4290 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004291
4292 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004293 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004294 parser.add_option_group(group)
4295
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004296 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004297 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004298 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004299 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004300 auth_config = auth.extract_auth_config_from_options(options)
4301
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004302
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004303 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004304 if options.newbranch:
4305 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004306 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004307 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004308
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004309 cl = Changelist(auth_config=auth_config,
4310 codereview=options.forced_codereview)
4311 if not cl.GetIssue():
4312 parser.error('current branch must have an associated issue')
4313
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004314 upstream = cl.GetUpstreamBranch()
4315 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004316 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004317
4318 RunGit(['reset', '--hard', upstream])
4319 if options.pull:
4320 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004321
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004322 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4323 options.directory)
4324
4325 if len(args) != 1 or not args[0]:
4326 parser.error('Must specify issue number or url')
4327
4328 # We don't want uncommitted changes mixed up with the patch.
4329 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004330 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004331
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004332 if options.newbranch:
4333 if options.force:
4334 RunGit(['branch', '-D', options.newbranch],
4335 stderr=subprocess2.PIPE, error_ok=True)
4336 RunGit(['new-branch', options.newbranch])
4337
4338 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4339
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004340 if cl.IsGerrit():
4341 if options.reject:
4342 parser.error('--reject is not supported with Gerrit codereview.')
4343 if options.nocommit:
4344 parser.error('--nocommit is not supported with Gerrit codereview.')
4345 if options.directory:
4346 parser.error('--directory is not supported with Gerrit codereview.')
4347
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004348 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004349 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004350
4351
4352def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004353 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004354 # Provide a wrapper for git svn rebase to help avoid accidental
4355 # git svn dcommit.
4356 # It's the only command that doesn't use parser at all since we just defer
4357 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004358
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004359 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004360
4361
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004362def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004363 """Fetches the tree status and returns either 'open', 'closed',
4364 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004365 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004366 if url:
4367 status = urllib2.urlopen(url).read().lower()
4368 if status.find('closed') != -1 or status == '0':
4369 return 'closed'
4370 elif status.find('open') != -1 or status == '1':
4371 return 'open'
4372 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004373 return 'unset'
4374
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004375
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004376def GetTreeStatusReason():
4377 """Fetches the tree status from a json url and returns the message
4378 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004379 url = settings.GetTreeStatusUrl()
4380 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004381 connection = urllib2.urlopen(json_url)
4382 status = json.loads(connection.read())
4383 connection.close()
4384 return status['message']
4385
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004386
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004387def GetBuilderMaster(bot_list):
4388 """For a given builder, fetch the master from AE if available."""
4389 map_url = 'https://builders-map.appspot.com/'
4390 try:
4391 master_map = json.load(urllib2.urlopen(map_url))
4392 except urllib2.URLError as e:
4393 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4394 (map_url, e))
4395 except ValueError as e:
4396 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4397 if not master_map:
4398 return None, 'Failed to build master map.'
4399
4400 result_master = ''
4401 for bot in bot_list:
4402 builder = bot.split(':', 1)[0]
4403 master_list = master_map.get(builder, [])
4404 if not master_list:
4405 return None, ('No matching master for builder %s.' % builder)
4406 elif len(master_list) > 1:
4407 return None, ('The builder name %s exists in multiple masters %s.' %
4408 (builder, master_list))
4409 else:
4410 cur_master = master_list[0]
4411 if not result_master:
4412 result_master = cur_master
4413 elif result_master != cur_master:
4414 return None, 'The builders do not belong to the same master.'
4415 return result_master, None
4416
4417
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004418def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004419 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004420 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004421 status = GetTreeStatus()
4422 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004423 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004424 return 2
4425
vapiera7fbd5a2016-06-16 09:17:49 -07004426 print('The tree is %s' % status)
4427 print()
4428 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004429 if status != 'open':
4430 return 1
4431 return 0
4432
4433
maruel@chromium.org15192402012-09-06 12:38:29 +00004434def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004435 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004436 group = optparse.OptionGroup(parser, "Try job options")
4437 group.add_option(
4438 "-b", "--bot", action="append",
4439 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4440 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004441 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004442 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004443 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004444 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004445 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004446 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004447 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004448 "-r", "--revision",
4449 help="Revision to use for the try job; default: the "
4450 "revision will be determined by the try server; see "
4451 "its waterfall for more info")
4452 group.add_option(
4453 "-c", "--clobber", action="store_true", default=False,
4454 help="Force a clobber before building; e.g. don't do an "
4455 "incremental build")
4456 group.add_option(
4457 "--project",
4458 help="Override which project to use. Projects are defined "
4459 "server-side to define what default bot set to use")
4460 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004461 "-p", "--property", dest="properties", action="append", default=[],
4462 help="Specify generic properties in the form -p key1=value1 -p "
4463 "key2=value2 etc (buildbucket only). The value will be treated as "
4464 "json if decodable, or as string otherwise.")
4465 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004466 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004467 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004468 "--use-rietveld", action="store_true", default=False,
4469 help="Use Rietveld to trigger try jobs.")
4470 group.add_option(
4471 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4472 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004473 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004474 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004475 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004476 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004477
machenbach@chromium.org45453142015-09-15 08:45:22 +00004478 if options.use_rietveld and options.properties:
4479 parser.error('Properties can only be specified with buildbucket')
4480
4481 # Make sure that all properties are prop=value pairs.
4482 bad_params = [x for x in options.properties if '=' not in x]
4483 if bad_params:
4484 parser.error('Got properties with missing "=": %s' % bad_params)
4485
maruel@chromium.org15192402012-09-06 12:38:29 +00004486 if args:
4487 parser.error('Unknown arguments: %s' % args)
4488
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004489 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004490 if not cl.GetIssue():
4491 parser.error('Need to upload first')
4492
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004493 if cl.IsGerrit():
4494 parser.error(
4495 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4496 'If your project has Commit Queue, dry run is a workaround:\n'
4497 ' git cl set-commit --dry-run')
4498 # Code below assumes Rietveld issue.
4499 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4500
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004501 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004502 if props.get('closed'):
4503 parser.error('Cannot send tryjobs for a closed CL')
4504
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004505 if props.get('private'):
4506 parser.error('Cannot use trybots with private issue')
4507
maruel@chromium.org15192402012-09-06 12:38:29 +00004508 if not options.name:
4509 options.name = cl.GetBranch()
4510
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004511 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004512 options.master, err_msg = GetBuilderMaster(options.bot)
4513 if err_msg:
4514 parser.error('Tryserver master cannot be found because: %s\n'
4515 'Please manually specify the tryserver master'
4516 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004517
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004518 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004519 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004520 if not options.bot:
4521 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004522
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004523 # Get try masters from PRESUBMIT.py files.
4524 masters = presubmit_support.DoGetTryMasters(
4525 change,
4526 change.LocalPaths(),
4527 settings.GetRoot(),
4528 None,
4529 None,
4530 options.verbose,
4531 sys.stdout)
4532 if masters:
4533 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004534
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004535 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4536 options.bot = presubmit_support.DoGetTrySlaves(
4537 change,
4538 change.LocalPaths(),
4539 settings.GetRoot(),
4540 None,
4541 None,
4542 options.verbose,
4543 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004544
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004545 if not options.bot:
tandrii9de9ec62016-07-13 03:01:59 -07004546 return {}
maruel@chromium.org15192402012-09-06 12:38:29 +00004547
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004548 builders_and_tests = {}
4549 # TODO(machenbach): The old style command-line options don't support
4550 # multiple try masters yet.
4551 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4552 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4553
4554 for bot in old_style:
4555 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004556 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004557 elif ',' in bot:
4558 parser.error('Specify one bot per --bot flag')
4559 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004560 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004561
4562 for bot, tests in new_style:
4563 builders_and_tests.setdefault(bot, []).extend(tests)
4564
4565 # Return a master map with one master to be backwards compatible. The
4566 # master name defaults to an empty string, which will cause the master
4567 # not to be set on rietveld (deprecated).
4568 return {options.master: builders_and_tests}
4569
4570 masters = GetMasterMap()
tandrii9de9ec62016-07-13 03:01:59 -07004571 if not masters:
4572 # Default to triggering Dry Run (see http://crbug.com/625697).
4573 if options.verbose:
4574 print('git cl try with no bots now defaults to CQ Dry Run.')
4575 try:
4576 cl.SetCQState(_CQState.DRY_RUN)
4577 print('scheduled CQ Dry Run on %s' % cl.GetIssueURL())
4578 return 0
4579 except KeyboardInterrupt:
4580 raise
4581 except:
4582 print('WARNING: failed to trigger CQ Dry Run.\n'
4583 'Either:\n'
4584 ' * your project has no CQ\n'
4585 ' * you don\'t have permission to trigger Dry Run\n'
4586 ' * bug in this code (see stack trace below).\n'
4587 'Consider specifying which bots to trigger manually '
4588 'or asking your project owners for permissions '
4589 'or contacting Chrome Infrastructure team at '
4590 'https://www.chromium.org/infra\n\n')
4591 # Still raise exception so that stack trace is printed.
4592 raise
stip@chromium.org43064fd2013-12-18 20:07:44 +00004593
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004594 for builders in masters.itervalues():
4595 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004596 print('ERROR You are trying to send a job to a triggered bot. This type '
4597 'of bot requires an\ninitial job from a parent (usually a builder).'
4598 ' Instead send your job to the parent.\n'
4599 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004600 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004601
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004602 patchset = cl.GetMostRecentPatchset()
4603 if patchset and patchset != cl.GetPatchset():
4604 print(
4605 '\nWARNING Mismatch between local config and server. Did a previous '
4606 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4607 'Continuing using\npatchset %s.\n' % patchset)
nodirabb9b222016-07-29 14:23:20 -07004608 if not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004609 try:
4610 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4611 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004612 print('ERROR: %s' % ex)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004613 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004614 except Exception as e:
4615 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004616 print('ERROR: Exception when trying to trigger tryjobs: %s\n%s' %
4617 (e, stacktrace))
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004618 return 1
4619 else:
4620 try:
4621 cl.RpcServer().trigger_distributed_try_jobs(
4622 cl.GetIssue(), patchset, options.name, options.clobber,
4623 options.revision, masters)
4624 except urllib2.HTTPError as e:
4625 if e.code == 404:
4626 print('404 from rietveld; '
4627 'did you mean to use "git try" instead of "git cl try"?')
4628 return 1
4629 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004630
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004631 for (master, builders) in sorted(masters.iteritems()):
4632 if master:
vapiera7fbd5a2016-06-16 09:17:49 -07004633 print('Master: %s' % master)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004634 length = max(len(builder) for builder in builders)
4635 for builder in sorted(builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004636 print(' %*s: %s' % (length, builder, ','.join(builders[builder])))
maruel@chromium.org15192402012-09-06 12:38:29 +00004637 return 0
4638
4639
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004640def CMDtry_results(parser, args):
4641 group = optparse.OptionGroup(parser, "Try job results options")
4642 group.add_option(
4643 "-p", "--patchset", type=int, help="patchset number if not current.")
4644 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004645 "--print-master", action='store_true', help="print master name as well.")
4646 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004647 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004648 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004649 group.add_option(
4650 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4651 help="Host of buildbucket. The default host is %default.")
4652 parser.add_option_group(group)
4653 auth.add_auth_options(parser)
4654 options, args = parser.parse_args(args)
4655 if args:
4656 parser.error('Unrecognized args: %s' % ' '.join(args))
4657
4658 auth_config = auth.extract_auth_config_from_options(options)
4659 cl = Changelist(auth_config=auth_config)
4660 if not cl.GetIssue():
4661 parser.error('Need to upload first')
4662
4663 if not options.patchset:
4664 options.patchset = cl.GetMostRecentPatchset()
4665 if options.patchset and options.patchset != cl.GetPatchset():
4666 print(
4667 '\nWARNING Mismatch between local config and server. Did a previous '
4668 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4669 'Continuing using\npatchset %s.\n' % options.patchset)
4670 try:
4671 jobs = fetch_try_jobs(auth_config, cl, options)
4672 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004673 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004674 return 1
4675 except Exception as e:
4676 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
vapiera7fbd5a2016-06-16 09:17:49 -07004677 print('ERROR: Exception when trying to fetch tryjobs: %s\n%s' %
4678 (e, stacktrace))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004679 return 1
4680 print_tryjobs(options, jobs)
4681 return 0
4682
4683
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004684@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004685def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004686 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004687 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004688 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004689 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004691 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004692 if args:
4693 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004694 branch = cl.GetBranch()
4695 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004696 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004697 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004698
4699 # Clear configured merge-base, if there is one.
4700 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004701 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004702 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004703 return 0
4704
4705
thestig@chromium.org00858c82013-12-02 23:08:03 +00004706def CMDweb(parser, args):
4707 """Opens the current CL in the web browser."""
4708 _, args = parser.parse_args(args)
4709 if args:
4710 parser.error('Unrecognized args: %s' % ' '.join(args))
4711
4712 issue_url = Changelist().GetIssueURL()
4713 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004714 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004715 return 1
4716
4717 webbrowser.open(issue_url)
4718 return 0
4719
4720
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004721def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004722 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004723 parser.add_option('-d', '--dry-run', action='store_true',
4724 help='trigger in dry run mode')
4725 parser.add_option('-c', '--clear', action='store_true',
4726 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004727 auth.add_auth_options(parser)
4728 options, args = parser.parse_args(args)
4729 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004730 if args:
4731 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004732 if options.dry_run and options.clear:
4733 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4734
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004735 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004736 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004737 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004738 elif options.dry_run:
4739 state = _CQState.DRY_RUN
4740 else:
4741 state = _CQState.COMMIT
4742 if not cl.GetIssue():
4743 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07004744 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004745 return 0
4746
4747
groby@chromium.org411034a2013-02-26 15:12:01 +00004748def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004749 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004750 auth.add_auth_options(parser)
4751 options, args = parser.parse_args(args)
4752 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004753 if args:
4754 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004755 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004756 # Ensure there actually is an issue to close.
4757 cl.GetDescription()
4758 cl.CloseIssue()
4759 return 0
4760
4761
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004762def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004763 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004764 auth.add_auth_options(parser)
4765 options, args = parser.parse_args(args)
4766 auth_config = auth.extract_auth_config_from_options(options)
4767 if args:
4768 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004769
4770 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004771 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004772 # Staged changes would be committed along with the patch from last
4773 # upload, hence counted toward the "last upload" side in the final
4774 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004775 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004776 return 1
4777
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004778 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004779 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004780 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004781 if not issue:
4782 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004783 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004784 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004785
4786 # Create a new branch based on the merge-base
4787 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004788 # Clear cached branch in cl object, to avoid overwriting original CL branch
4789 # properties.
4790 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004791 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004792 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004793 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004794 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004795 return rtn
4796
wychen@chromium.org06928532015-02-03 02:11:29 +00004797 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004798 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004799 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004800 finally:
4801 RunGit(['checkout', '-q', branch])
4802 RunGit(['branch', '-D', TMP_BRANCH])
4803
4804 return 0
4805
4806
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004807def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004808 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004809 parser.add_option(
4810 '--no-color',
4811 action='store_true',
4812 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004813 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004814 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004815 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004816
4817 author = RunGit(['config', 'user.email']).strip() or None
4818
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004819 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004820
4821 if args:
4822 if len(args) > 1:
4823 parser.error('Unknown args')
4824 base_branch = args[0]
4825 else:
4826 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004827 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004828
4829 change = cl.GetChange(base_branch, None)
4830 return owners_finder.OwnersFinder(
4831 [f.LocalPath() for f in
4832 cl.GetChange(base_branch, None).AffectedFiles()],
4833 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07004834 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004835 disable_color=options.no_color).run()
4836
4837
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004838def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004839 """Generates a diff command."""
4840 # Generate diff for the current branch's changes.
4841 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4842 upstream_commit, '--' ]
4843
4844 if args:
4845 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004846 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004847 diff_cmd.append(arg)
4848 else:
4849 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004850
4851 return diff_cmd
4852
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004853def MatchingFileType(file_name, extensions):
4854 """Returns true if the file name ends with one of the given extensions."""
4855 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004856
enne@chromium.org555cfe42014-01-29 18:21:39 +00004857@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004858def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004859 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07004860 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07004861 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004862 parser.add_option('--full', action='store_true',
4863 help='Reformat the full content of all touched files')
4864 parser.add_option('--dry-run', action='store_true',
4865 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004866 parser.add_option('--python', action='store_true',
4867 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004868 parser.add_option('--diff', action='store_true',
4869 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004870 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004871
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004872 # git diff generates paths against the root of the repository. Change
4873 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004874 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004875 if rel_base_path:
4876 os.chdir(rel_base_path)
4877
digit@chromium.org29e47272013-05-17 17:01:46 +00004878 # Grab the merge-base commit, i.e. the upstream commit of the current
4879 # branch when it was created or the last time it was rebased. This is
4880 # to cover the case where the user may have called "git fetch origin",
4881 # moving the origin branch to a newer commit, but hasn't rebased yet.
4882 upstream_commit = None
4883 cl = Changelist()
4884 upstream_branch = cl.GetUpstreamBranch()
4885 if upstream_branch:
4886 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4887 upstream_commit = upstream_commit.strip()
4888
4889 if not upstream_commit:
4890 DieWithError('Could not find base commit for this branch. '
4891 'Are you in detached state?')
4892
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004893 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4894 diff_output = RunGit(changed_files_cmd)
4895 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004896 # Filter out files deleted by this CL
4897 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004898
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004899 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4900 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4901 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004902 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004903
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004904 top_dir = os.path.normpath(
4905 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4906
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004907 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4908 # formatted. This is used to block during the presubmit.
4909 return_value = 0
4910
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004911 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004912 # Locate the clang-format binary in the checkout
4913 try:
4914 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07004915 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004916 DieWithError(e)
4917
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004918 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004919 cmd = [clang_format_tool]
4920 if not opts.dry_run and not opts.diff:
4921 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004922 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004923 if opts.diff:
4924 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004925 else:
4926 env = os.environ.copy()
4927 env['PATH'] = str(os.path.dirname(clang_format_tool))
4928 try:
4929 script = clang_format.FindClangFormatScriptInChromiumTree(
4930 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07004931 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004932 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004933
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004934 cmd = [sys.executable, script, '-p0']
4935 if not opts.dry_run and not opts.diff:
4936 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004937
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004938 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4939 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004940
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004941 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4942 if opts.diff:
4943 sys.stdout.write(stdout)
4944 if opts.dry_run and len(stdout) > 0:
4945 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004946
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004947 # Similar code to above, but using yapf on .py files rather than clang-format
4948 # on C/C++ files
4949 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004950 yapf_tool = gclient_utils.FindExecutable('yapf')
4951 if yapf_tool is None:
4952 DieWithError('yapf not found in PATH')
4953
4954 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004955 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004956 cmd = [yapf_tool]
4957 if not opts.dry_run and not opts.diff:
4958 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004959 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004960 if opts.diff:
4961 sys.stdout.write(stdout)
4962 else:
4963 # TODO(sbc): yapf --lines mode still has some issues.
4964 # https://github.com/google/yapf/issues/154
4965 DieWithError('--python currently only works with --full')
4966
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004967 # Dart's formatter does not have the nice property of only operating on
4968 # modified chunks, so hard code full.
4969 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004970 try:
4971 command = [dart_format.FindDartFmtToolInChromiumTree()]
4972 if not opts.dry_run and not opts.diff:
4973 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004974 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004975
ppi@chromium.org6593d932016-03-03 15:41:15 +00004976 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004977 if opts.dry_run and stdout:
4978 return_value = 2
4979 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07004980 print('Warning: Unable to check Dart code formatting. Dart SDK not '
4981 'found in this checkout. Files in other languages are still '
4982 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004983
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004984 # Format GN build files. Always run on full build files for canonical form.
4985 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07004986 cmd = ['gn', 'format' ]
4987 if opts.dry_run or opts.diff:
4988 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004989 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07004990 gn_ret = subprocess2.call(cmd + [gn_diff_file],
4991 shell=sys.platform == 'win32',
4992 cwd=top_dir)
4993 if opts.dry_run and gn_ret == 2:
4994 return_value = 2 # Not formatted.
4995 elif opts.diff and gn_ret == 2:
4996 # TODO this should compute and print the actual diff.
4997 print("This change has GN build file diff for " + gn_diff_file)
4998 elif gn_ret != 0:
4999 # For non-dry run cases (and non-2 return values for dry-run), a
5000 # nonzero error code indicates a failure, probably because the file
5001 # doesn't parse.
5002 DieWithError("gn format failed on " + gn_diff_file +
5003 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005004
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005005 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005006
5007
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005008@subcommand.usage('<codereview url or issue id>')
5009def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005010 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005011 _, args = parser.parse_args(args)
5012
5013 if len(args) != 1:
5014 parser.print_help()
5015 return 1
5016
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005017 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005018 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005019 parser.print_help()
5020 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005021 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005022
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005023 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005024 output = RunGit(['config', '--local', '--get-regexp',
5025 r'branch\..*\.%s' % issueprefix],
5026 error_ok=True)
5027 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005028 if issue == target_issue:
5029 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005030
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005031 branches = []
5032 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00005033 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005034 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005035 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005036 return 1
5037 if len(branches) == 1:
5038 RunGit(['checkout', branches[0]])
5039 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005040 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005041 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005042 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005043 which = raw_input('Choose by index: ')
5044 try:
5045 RunGit(['checkout', branches[int(which)]])
5046 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005047 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005048 return 1
5049
5050 return 0
5051
5052
maruel@chromium.org29404b52014-09-08 22:58:00 +00005053def CMDlol(parser, args):
5054 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005055 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005056 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5057 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5058 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005059 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005060 return 0
5061
5062
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005063class OptionParser(optparse.OptionParser):
5064 """Creates the option parse and add --verbose support."""
5065 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005066 optparse.OptionParser.__init__(
5067 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005068 self.add_option(
5069 '-v', '--verbose', action='count', default=0,
5070 help='Use 2 times for more debugging info')
5071
5072 def parse_args(self, args=None, values=None):
5073 options, args = optparse.OptionParser.parse_args(self, args, values)
5074 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5075 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5076 return options, args
5077
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005078
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005079def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005080 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005081 print('\nYour python version %s is unsupported, please upgrade.\n' %
5082 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005083 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005084
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005085 # Reload settings.
5086 global settings
5087 settings = Settings()
5088
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005089 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005090 dispatcher = subcommand.CommandDispatcher(__name__)
5091 try:
5092 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005093 except auth.AuthenticationError as e:
5094 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005095 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005096 if e.code != 500:
5097 raise
5098 DieWithError(
5099 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5100 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005101 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005102
5103
5104if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005105 # These affect sys.stdout so do it outside of main() to simplify mocks in
5106 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005107 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005108 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005109 try:
5110 sys.exit(main(sys.argv[1:]))
5111 except KeyboardInterrupt:
5112 sys.stderr.write('interrupted\n')
5113 sys.exit(1)