blob: fd2dcea4189f99632da41dafc2874c857f599091 [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
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
tandrii@chromium.org04ea8462016-04-25 19:51:21 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
tandrii@chromium.org04ea8462016-04-25 19:51:21 +000024import tempfile
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
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
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):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
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
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **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):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 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
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.orgc68f7b52016-04-28 19:42:47 +0000340 if 'presubmit' in builder.lower():
341 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000342 if tests:
343 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000344 if properties:
345 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000346 if options.clobber:
347 parameters['properties']['clobber'] = True
348 batch_req_body['builds'].append(
349 {
350 'bucket': bucket,
351 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000353 'tags': ['builder:%s' % builder,
354 'buildset:%s' % buildset,
355 'master:%s' % master,
356 'user_agent:git_cl_try']
357 }
358 )
359
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360 _buildbucket_retry(
361 'triggering tryjobs',
362 http,
363 buildbucket_put_url,
364 'PUT',
365 body=json.dumps(batch_req_body),
366 headers={'Content-Type': 'application/json'}
367 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000368 print_text.append('To see results here, run: git cl try-results')
369 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000371
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000372
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000373def fetch_try_jobs(auth_config, changelist, options):
374 """Fetches tryjobs from buildbucket.
375
376 Returns a map from build id to build info as json dictionary.
377 """
378 rietveld_url = settings.GetDefaultServerUrl()
379 rietveld_host = urlparse.urlparse(rietveld_url).hostname
380 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
381 if authenticator.has_cached_credentials():
382 http = authenticator.authorize(httplib2.Http())
383 else:
384 print ('Warning: Some results might be missing because %s' %
385 # Get the message on how to login.
386 auth.LoginRequiredError(rietveld_host).message)
387 http = httplib2.Http()
388
389 http.force_exception_to_status_code = True
390
391 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
392 hostname=rietveld_host,
393 issue=changelist.GetIssue(),
394 patch=options.patchset)
395 params = {'tag': 'buildset:%s' % buildset}
396
397 builds = {}
398 while True:
399 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
400 hostname=options.buildbucket_host,
401 params=urllib.urlencode(params))
402 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
403 for build in content.get('builds', []):
404 builds[build['id']] = build
405 if 'next_cursor' in content:
406 params['start_cursor'] = content['next_cursor']
407 else:
408 break
409 return builds
410
411
412def print_tryjobs(options, builds):
413 """Prints nicely result of fetch_try_jobs."""
414 if not builds:
415 print 'No tryjobs scheduled'
416 return
417
418 # Make a copy, because we'll be modifying builds dictionary.
419 builds = builds.copy()
420 builder_names_cache = {}
421
422 def get_builder(b):
423 try:
424 return builder_names_cache[b['id']]
425 except KeyError:
426 try:
427 parameters = json.loads(b['parameters_json'])
428 name = parameters['builder_name']
429 except (ValueError, KeyError) as error:
430 print 'WARNING: failed to get builder name for build %s: %s' % (
431 b['id'], error)
432 name = None
433 builder_names_cache[b['id']] = name
434 return name
435
436 def get_bucket(b):
437 bucket = b['bucket']
438 if bucket.startswith('master.'):
439 return bucket[len('master.'):]
440 return bucket
441
442 if options.print_master:
443 name_fmt = '%%-%ds %%-%ds' % (
444 max(len(str(get_bucket(b))) for b in builds.itervalues()),
445 max(len(str(get_builder(b))) for b in builds.itervalues()))
446 def get_name(b):
447 return name_fmt % (get_bucket(b), get_builder(b))
448 else:
449 name_fmt = '%%-%ds' % (
450 max(len(str(get_builder(b))) for b in builds.itervalues()))
451 def get_name(b):
452 return name_fmt % get_builder(b)
453
454 def sort_key(b):
455 return b['status'], b.get('result'), get_name(b), b.get('url')
456
457 def pop(title, f, color=None, **kwargs):
458 """Pop matching builds from `builds` dict and print them."""
459
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000460 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461 colorize = str
462 else:
463 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
464
465 result = []
466 for b in builds.values():
467 if all(b.get(k) == v for k, v in kwargs.iteritems()):
468 builds.pop(b['id'])
469 result.append(b)
470 if result:
471 print colorize(title)
472 for b in sorted(result, key=sort_key):
473 print ' ', colorize('\t'.join(map(str, f(b))))
474
475 total = len(builds)
476 pop(status='COMPLETED', result='SUCCESS',
477 title='Successes:', color=Fore.GREEN,
478 f=lambda b: (get_name(b), b.get('url')))
479 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
480 title='Infra Failures:', color=Fore.MAGENTA,
481 f=lambda b: (get_name(b), b.get('url')))
482 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
483 title='Failures:', color=Fore.RED,
484 f=lambda b: (get_name(b), b.get('url')))
485 pop(status='COMPLETED', result='CANCELED',
486 title='Canceled:', color=Fore.MAGENTA,
487 f=lambda b: (get_name(b),))
488 pop(status='COMPLETED', result='FAILURE',
489 failure_reason='INVALID_BUILD_DEFINITION',
490 title='Wrong master/builder name:', color=Fore.MAGENTA,
491 f=lambda b: (get_name(b),))
492 pop(status='COMPLETED', result='FAILURE',
493 title='Other failures:',
494 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
495 pop(status='COMPLETED',
496 title='Other finished:',
497 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
498 pop(status='STARTED',
499 title='Started:', color=Fore.YELLOW,
500 f=lambda b: (get_name(b), b.get('url')))
501 pop(status='SCHEDULED',
502 title='Scheduled:',
503 f=lambda b: (get_name(b), 'id=%s' % b['id']))
504 # The last section is just in case buildbucket API changes OR there is a bug.
505 pop(title='Other:',
506 f=lambda b: (get_name(b), 'id=%s' % b['id']))
507 assert len(builds) == 0
508 print 'Total: %d tryjobs' % total
509
510
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000511def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
512 """Return the corresponding git ref if |base_url| together with |glob_spec|
513 matches the full |url|.
514
515 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
516 """
517 fetch_suburl, as_ref = glob_spec.split(':')
518 if allow_wildcards:
519 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
520 if glob_match:
521 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
522 # "branches/{472,597,648}/src:refs/remotes/svn/*".
523 branch_re = re.escape(base_url)
524 if glob_match.group(1):
525 branch_re += '/' + re.escape(glob_match.group(1))
526 wildcard = glob_match.group(2)
527 if wildcard == '*':
528 branch_re += '([^/]*)'
529 else:
530 # Escape and replace surrounding braces with parentheses and commas
531 # with pipe symbols.
532 wildcard = re.escape(wildcard)
533 wildcard = re.sub('^\\\\{', '(', wildcard)
534 wildcard = re.sub('\\\\,', '|', wildcard)
535 wildcard = re.sub('\\\\}$', ')', wildcard)
536 branch_re += wildcard
537 if glob_match.group(3):
538 branch_re += re.escape(glob_match.group(3))
539 match = re.match(branch_re, url)
540 if match:
541 return re.sub('\*$', match.group(1), as_ref)
542
543 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
544 if fetch_suburl:
545 full_url = base_url + '/' + fetch_suburl
546 else:
547 full_url = base_url
548 if full_url == url:
549 return as_ref
550 return None
551
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000552
iannucci@chromium.org79540052012-10-19 23:15:26 +0000553def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000554 """Prints statistics about the change to the user."""
555 # --no-ext-diff is broken in some versions of Git, so try to work around
556 # this by overriding the environment (but there is still a problem if the
557 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000558 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000559 if 'GIT_EXTERNAL_DIFF' in env:
560 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000561
562 if find_copies:
563 similarity_options = ['--find-copies-harder', '-l100000',
564 '-C%s' % similarity]
565 else:
566 similarity_options = ['-M%s' % similarity]
567
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000568 try:
569 stdout = sys.stdout.fileno()
570 except AttributeError:
571 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000572 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000573 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000574 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000575 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000576
577
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000578class BuildbucketResponseException(Exception):
579 pass
580
581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000582class Settings(object):
583 def __init__(self):
584 self.default_server = None
585 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000586 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 self.is_git_svn = None
588 self.svn_branch = None
589 self.tree_status_url = None
590 self.viewvc_url = None
591 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000592 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000593 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000594 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000595 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000596 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000597 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000598 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599
600 def LazyUpdateIfNeeded(self):
601 """Updates the settings from a codereview.settings file, if available."""
602 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 # The only value that actually changes the behavior is
604 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000605 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000606 error_ok=True
607 ).strip().lower()
608
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000609 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000610 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 LoadCodereviewSettingsFromFile(cr_settings_file)
612 self.updated = True
613
614 def GetDefaultServerUrl(self, error_ok=False):
615 if not self.default_server:
616 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000617 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000618 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 if error_ok:
620 return self.default_server
621 if not self.default_server:
622 error_message = ('Could not find settings file. You must configure '
623 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000624 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000625 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 return self.default_server
627
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000628 @staticmethod
629 def GetRelativeRoot():
630 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000633 if self.root is None:
634 self.root = os.path.abspath(self.GetRelativeRoot())
635 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000636
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 def GetGitMirror(self, remote='origin'):
638 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000639 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000640 if not os.path.isdir(local_url):
641 return None
642 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
643 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
644 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
645 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
646 if mirror.exists():
647 return mirror
648 return None
649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 def GetIsGitSvn(self):
651 """Return true if this repo looks like it's using git-svn."""
652 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000653 if self.GetPendingRefPrefix():
654 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
655 self.is_git_svn = False
656 else:
657 # If you have any "svn-remote.*" config keys, we think you're using svn.
658 self.is_git_svn = RunGitWithCode(
659 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 return self.is_git_svn
661
662 def GetSVNBranch(self):
663 if self.svn_branch is None:
664 if not self.GetIsGitSvn():
665 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
666
667 # Try to figure out which remote branch we're based on.
668 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000669 # 1) iterate through our branch history and find the svn URL.
670 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671
672 # regexp matching the git-svn line that contains the URL.
673 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
674
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000675 # We don't want to go through all of history, so read a line from the
676 # pipe at a time.
677 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000678 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000679 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
680 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000681 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000682 for line in proc.stdout:
683 match = git_svn_re.match(line)
684 if match:
685 url = match.group(1)
686 proc.stdout.close() # Cut pipe.
687 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000688
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000689 if url:
690 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
691 remotes = RunGit(['config', '--get-regexp',
692 r'^svn-remote\..*\.url']).splitlines()
693 for remote in remotes:
694 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000696 remote = match.group(1)
697 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000698 rewrite_root = RunGit(
699 ['config', 'svn-remote.%s.rewriteRoot' % remote],
700 error_ok=True).strip()
701 if rewrite_root:
702 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000703 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000704 ['config', 'svn-remote.%s.fetch' % remote],
705 error_ok=True).strip()
706 if fetch_spec:
707 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
708 if self.svn_branch:
709 break
710 branch_spec = RunGit(
711 ['config', 'svn-remote.%s.branches' % remote],
712 error_ok=True).strip()
713 if branch_spec:
714 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
715 if self.svn_branch:
716 break
717 tag_spec = RunGit(
718 ['config', 'svn-remote.%s.tags' % remote],
719 error_ok=True).strip()
720 if tag_spec:
721 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
722 if self.svn_branch:
723 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724
725 if not self.svn_branch:
726 DieWithError('Can\'t guess svn branch -- try specifying it on the '
727 'command line')
728
729 return self.svn_branch
730
731 def GetTreeStatusUrl(self, error_ok=False):
732 if not self.tree_status_url:
733 error_message = ('You must configure your tree status URL by running '
734 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000735 self.tree_status_url = self._GetRietveldConfig(
736 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 return self.tree_status_url
738
739 def GetViewVCUrl(self):
740 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742 return self.viewvc_url
743
rmistry@google.com90752582014-01-14 21:04:50 +0000744 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000745 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000746
rmistry@google.com78948ed2015-07-08 23:09:57 +0000747 def GetIsSkipDependencyUpload(self, branch_name):
748 """Returns true if specified branch should skip dep uploads."""
749 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
750 error_ok=True)
751
rmistry@google.com5626a922015-02-26 14:03:30 +0000752 def GetRunPostUploadHook(self):
753 run_post_upload_hook = self._GetRietveldConfig(
754 'run-post-upload-hook', error_ok=True)
755 return run_post_upload_hook == "True"
756
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000757 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000759
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000760 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000761 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000762
ukai@chromium.orge8077812012-02-03 03:41:46 +0000763 def GetIsGerrit(self):
764 """Return true if this repo is assosiated with gerrit code review system."""
765 if self.is_gerrit is None:
766 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
767 return self.is_gerrit
768
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000769 def GetSquashGerritUploads(self):
770 """Return true if uploads to Gerrit should be squashed by default."""
771 if self.squash_gerrit_uploads is None:
772 self.squash_gerrit_uploads = (
773 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
774 error_ok=True).strip() == 'true')
775 return self.squash_gerrit_uploads
776
tandrii@chromium.org28253532016-04-14 13:46:56 +0000777 def GetGerritSkipEnsureAuthenticated(self):
778 """Return True if EnsureAuthenticated should not be done for Gerrit
779 uploads."""
780 if self.gerrit_skip_ensure_authenticated is None:
781 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000782 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000783 error_ok=True).strip() == 'true')
784 return self.gerrit_skip_ensure_authenticated
785
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000786 def GetGitEditor(self):
787 """Return the editor specified in the git config, or None if none is."""
788 if self.git_editor is None:
789 self.git_editor = self._GetConfig('core.editor', error_ok=True)
790 return self.git_editor or None
791
thestig@chromium.org44202a22014-03-11 19:22:18 +0000792 def GetLintRegex(self):
793 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
794 DEFAULT_LINT_REGEX)
795
796 def GetLintIgnoreRegex(self):
797 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
798 DEFAULT_LINT_IGNORE_REGEX)
799
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000800 def GetProject(self):
801 if not self.project:
802 self.project = self._GetRietveldConfig('project', error_ok=True)
803 return self.project
804
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000805 def GetForceHttpsCommitUrl(self):
806 if not self.force_https_commit_url:
807 self.force_https_commit_url = self._GetRietveldConfig(
808 'force-https-commit-url', error_ok=True)
809 return self.force_https_commit_url
810
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000811 def GetPendingRefPrefix(self):
812 if not self.pending_ref_prefix:
813 self.pending_ref_prefix = self._GetRietveldConfig(
814 'pending-ref-prefix', error_ok=True)
815 return self.pending_ref_prefix
816
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000817 def _GetRietveldConfig(self, param, **kwargs):
818 return self._GetConfig('rietveld.' + param, **kwargs)
819
rmistry@google.com78948ed2015-07-08 23:09:57 +0000820 def _GetBranchConfig(self, branch_name, param, **kwargs):
821 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
822
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823 def _GetConfig(self, param, **kwargs):
824 self.LazyUpdateIfNeeded()
825 return RunGit(['config', param], **kwargs).strip()
826
827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828def ShortBranchName(branch):
829 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000830 return branch.replace('refs/heads/', '', 1)
831
832
833def GetCurrentBranchRef():
834 """Returns branch ref (e.g., refs/heads/master) or None."""
835 return RunGit(['symbolic-ref', 'HEAD'],
836 stderr=subprocess2.VOID, error_ok=True).strip() or None
837
838
839def GetCurrentBranch():
840 """Returns current branch or None.
841
842 For refs/heads/* branches, returns just last part. For others, full ref.
843 """
844 branchref = GetCurrentBranchRef()
845 if branchref:
846 return ShortBranchName(branchref)
847 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
849
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000850class _CQState(object):
851 """Enum for states of CL with respect to Commit Queue."""
852 NONE = 'none'
853 DRY_RUN = 'dry_run'
854 COMMIT = 'commit'
855
856 ALL_STATES = [NONE, DRY_RUN, COMMIT]
857
858
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000859class _ParsedIssueNumberArgument(object):
860 def __init__(self, issue=None, patchset=None, hostname=None):
861 self.issue = issue
862 self.patchset = patchset
863 self.hostname = hostname
864
865 @property
866 def valid(self):
867 return self.issue is not None
868
869
870class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
871 def __init__(self, *args, **kwargs):
872 self.patch_url = kwargs.pop('patch_url', None)
873 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
874
875
876def ParseIssueNumberArgument(arg):
877 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
878 fail_result = _ParsedIssueNumberArgument()
879
880 if arg.isdigit():
881 return _ParsedIssueNumberArgument(issue=int(arg))
882 if not arg.startswith('http'):
883 return fail_result
884 url = gclient_utils.UpgradeToHttps(arg)
885 try:
886 parsed_url = urlparse.urlparse(url)
887 except ValueError:
888 return fail_result
889 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
890 tmp = cls.ParseIssueURL(parsed_url)
891 if tmp is not None:
892 return tmp
893 return fail_result
894
895
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000897 """Changelist works with one changelist in local branch.
898
899 Supports two codereview backends: Rietveld or Gerrit, selected at object
900 creation.
901
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000902 Notes:
903 * Not safe for concurrent multi-{thread,process} use.
904 * Caches values from current branch. Therefore, re-use after branch change
905 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000906 """
907
908 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
909 """Create a new ChangeList instance.
910
911 If issue is given, the codereview must be given too.
912
913 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
914 Otherwise, it's decided based on current configuration of the local branch,
915 with default being 'rietveld' for backwards compatibility.
916 See _load_codereview_impl for more details.
917
918 **kwargs will be passed directly to codereview implementation.
919 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000920 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000921 global settings
922 if not settings:
923 # Happens when git_cl.py is used as a utility library.
924 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000925
926 if issue:
927 assert codereview, 'codereview must be known, if issue is known'
928
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 self.branchref = branchref
930 if self.branchref:
931 self.branch = ShortBranchName(self.branchref)
932 else:
933 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000935 self.lookedup_issue = False
936 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937 self.has_description = False
938 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000939 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000941 self.cc = None
942 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000943 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000944
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000946 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000947 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000948 assert self._codereview_impl
949 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000950
951 def _load_codereview_impl(self, codereview=None, **kwargs):
952 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000953 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
954 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
955 self._codereview = codereview
956 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000957 return
958
959 # Automatic selection based on issue number set for a current branch.
960 # Rietveld takes precedence over Gerrit.
961 assert not self.issue
962 # Whether we find issue or not, we are doing the lookup.
963 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000964 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000965 setting = cls.IssueSetting(self.GetBranch())
966 issue = RunGit(['config', setting], error_ok=True).strip()
967 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000968 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969 self._codereview_impl = cls(self, **kwargs)
970 self.issue = int(issue)
971 return
972
973 # No issue is set for this branch, so decide based on repo-wide settings.
974 return self._load_codereview_impl(
975 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
976 **kwargs)
977
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000978 def IsGerrit(self):
979 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000980
981 def GetCCList(self):
982 """Return the users cc'd on this CL.
983
984 Return is a string suitable for passing to gcl with the --cc flag.
985 """
986 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000987 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000988 more_cc = ','.join(self.watchers)
989 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
990 return self.cc
991
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000992 def GetCCListWithoutDefault(self):
993 """Return the users cc'd on this CL excluding default ones."""
994 if self.cc is None:
995 self.cc = ','.join(self.watchers)
996 return self.cc
997
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000998 def SetWatchers(self, watchers):
999 """Set the list of email addresses that should be cc'd based on the changed
1000 files in this CL.
1001 """
1002 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003
1004 def GetBranch(self):
1005 """Returns the short branch name, e.g. 'master'."""
1006 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001007 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001008 if not branchref:
1009 return None
1010 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 self.branch = ShortBranchName(self.branchref)
1012 return self.branch
1013
1014 def GetBranchRef(self):
1015 """Returns the full branch name, e.g. 'refs/heads/master'."""
1016 self.GetBranch() # Poke the lazy loader.
1017 return self.branchref
1018
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001019 def ClearBranch(self):
1020 """Clears cached branch data of this object."""
1021 self.branch = self.branchref = None
1022
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001023 @staticmethod
1024 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001025 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 e.g. 'origin', 'refs/heads/master'
1027 """
1028 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1030 error_ok=True).strip()
1031 if upstream_branch:
1032 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1033 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001034 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1035 error_ok=True).strip()
1036 if upstream_branch:
1037 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001039 # Fall back on trying a git-svn upstream branch.
1040 if settings.GetIsGitSvn():
1041 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001043 # Else, try to guess the origin remote.
1044 remote_branches = RunGit(['branch', '-r']).split()
1045 if 'origin/master' in remote_branches:
1046 # Fall back on origin/master if it exits.
1047 remote = 'origin'
1048 upstream_branch = 'refs/heads/master'
1049 elif 'origin/trunk' in remote_branches:
1050 # Fall back on origin/trunk if it exists. Generally a shared
1051 # git-svn clone
1052 remote = 'origin'
1053 upstream_branch = 'refs/heads/trunk'
1054 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001055 DieWithError(
1056 'Unable to determine default branch to diff against.\n'
1057 'Either pass complete "git diff"-style arguments, like\n'
1058 ' git cl upload origin/master\n'
1059 'or verify this branch is set up to track another \n'
1060 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061
1062 return remote, upstream_branch
1063
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001064 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001065 upstream_branch = self.GetUpstreamBranch()
1066 if not BranchExists(upstream_branch):
1067 DieWithError('The upstream for the current branch (%s) does not exist '
1068 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001069 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001070 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001071
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 def GetUpstreamBranch(self):
1073 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001076 upstream_branch = upstream_branch.replace('refs/heads/',
1077 'refs/remotes/%s/' % remote)
1078 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1079 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 self.upstream_branch = upstream_branch
1081 return self.upstream_branch
1082
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001083 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001084 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001085 remote, branch = None, self.GetBranch()
1086 seen_branches = set()
1087 while branch not in seen_branches:
1088 seen_branches.add(branch)
1089 remote, branch = self.FetchUpstreamTuple(branch)
1090 branch = ShortBranchName(branch)
1091 if remote != '.' or branch.startswith('refs/remotes'):
1092 break
1093 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001094 remotes = RunGit(['remote'], error_ok=True).split()
1095 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001096 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001097 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001099 logging.warning('Could not determine which remote this change is '
1100 'associated with, so defaulting to "%s". This may '
1101 'not be what you want. You may prevent this message '
1102 'by running "git svn info" as documented here: %s',
1103 self._remote,
1104 GIT_INSTRUCTIONS_URL)
1105 else:
1106 logging.warn('Could not determine which remote this change is '
1107 'associated with. You may prevent this message by '
1108 'running "git svn info" as documented here: %s',
1109 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001110 branch = 'HEAD'
1111 if branch.startswith('refs/remotes'):
1112 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001113 elif branch.startswith('refs/branch-heads/'):
1114 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 else:
1116 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001117 return self._remote
1118
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001119 def GitSanityChecks(self, upstream_git_obj):
1120 """Checks git repo status and ensures diff is from local commits."""
1121
sbc@chromium.org79706062015-01-14 21:18:12 +00001122 if upstream_git_obj is None:
1123 if self.GetBranch() is None:
1124 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001125 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001126 else:
1127 print >> sys.stderr, (
1128 'ERROR: no upstream branch')
1129 return False
1130
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001131 # Verify the commit we're diffing against is in our current branch.
1132 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1133 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1134 if upstream_sha != common_ancestor:
1135 print >> sys.stderr, (
1136 'ERROR: %s is not in the current branch. You may need to rebase '
1137 'your tracking branch' % upstream_sha)
1138 return False
1139
1140 # List the commits inside the diff, and verify they are all local.
1141 commits_in_diff = RunGit(
1142 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1143 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1144 remote_branch = remote_branch.strip()
1145 if code != 0:
1146 _, remote_branch = self.GetRemoteBranch()
1147
1148 commits_in_remote = RunGit(
1149 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1150
1151 common_commits = set(commits_in_diff) & set(commits_in_remote)
1152 if common_commits:
1153 print >> sys.stderr, (
1154 'ERROR: Your diff contains %d commits already in %s.\n'
1155 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1156 'the diff. If you are using a custom git flow, you can override'
1157 ' the reference used for this check with "git config '
1158 'gitcl.remotebranch <git-ref>".' % (
1159 len(common_commits), remote_branch, upstream_git_obj))
1160 return False
1161 return True
1162
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001163 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001164 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001165
1166 Returns None if it is not set.
1167 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001168 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1169 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001170
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001171 def GetGitSvnRemoteUrl(self):
1172 """Return the configured git-svn remote URL parsed from git svn info.
1173
1174 Returns None if it is not set.
1175 """
1176 # URL is dependent on the current directory.
1177 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1178 if data:
1179 keys = dict(line.split(': ', 1) for line in data.splitlines()
1180 if ': ' in line)
1181 return keys.get('URL', None)
1182 return None
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 def GetRemoteUrl(self):
1185 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1186
1187 Returns None if there is no remote.
1188 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001190 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1191
1192 # If URL is pointing to a local directory, it is probably a git cache.
1193 if os.path.isdir(url):
1194 url = RunGit(['config', 'remote.%s.url' % remote],
1195 error_ok=True,
1196 cwd=url).strip()
1197 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001199 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001200 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001201 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 issue = RunGit(['config',
1203 self._codereview_impl.IssueSetting(self.GetBranch())],
1204 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 self.issue = int(issue) or None if issue else None
1206 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 return self.issue
1208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 def GetIssueURL(self):
1210 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001211 issue = self.GetIssue()
1212 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001213 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
1216 def GetDescription(self, pretty=False):
1217 if not self.has_description:
1218 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 self.has_description = True
1221 if pretty:
1222 wrapper = textwrap.TextWrapper()
1223 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1224 return wrapper.fill(self.description)
1225 return self.description
1226
1227 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001228 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001230 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 self.patchset = int(patchset) or None if patchset else None
1233 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 return self.patchset
1235
1236 def SetPatchset(self, patchset):
1237 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001238 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001240 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001241 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001243 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001244 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001245 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001247 def SetIssue(self, issue=None):
1248 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001249 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1250 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001252 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001253 RunGit(['config', issue_setting, str(issue)])
1254 codereview_server = self._codereview_impl.GetCodereviewServer()
1255 if codereview_server:
1256 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001258 current_issue = self.GetIssue()
1259 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001260 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001261 self.issue = None
1262 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001264 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001265 if not self.GitSanityChecks(upstream_branch):
1266 DieWithError('\nGit sanity check failure')
1267
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001268 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001269 if not root:
1270 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001271 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001272
1273 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001274 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001275 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001276 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001277 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001278 except subprocess2.CalledProcessError:
1279 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001280 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001281 'This branch probably doesn\'t exist anymore. To reset the\n'
1282 'tracking branch, please run\n'
1283 ' git branch --set-upstream %s trunk\n'
1284 'replacing trunk with origin/master or the relevant branch') %
1285 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001286
maruel@chromium.org52424302012-08-29 15:14:30 +00001287 issue = self.GetIssue()
1288 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001289 if issue:
1290 description = self.GetDescription()
1291 else:
1292 # If the change was never uploaded, use the log messages of all commits
1293 # up to the branch point, as git cl upload will prefill the description
1294 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001295 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1296 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001297
1298 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001299 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001300 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001301 name,
1302 description,
1303 absroot,
1304 files,
1305 issue,
1306 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001307 author,
1308 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001309
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310 def UpdateDescription(self, description):
1311 self.description = description
1312 return self._codereview_impl.UpdateDescriptionRemote(description)
1313
1314 def RunHook(self, committing, may_prompt, verbose, change):
1315 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1316 try:
1317 return presubmit_support.DoPresubmitChecks(change, committing,
1318 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1319 default_presubmit=None, may_prompt=may_prompt,
1320 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1321 except presubmit_support.PresubmitFailure, e:
1322 DieWithError(
1323 ('%s\nMaybe your depot_tools is out of date?\n'
1324 'If all fails, contact maruel@') % e)
1325
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001326 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1327 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001328 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1329 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001330 else:
1331 # Assume url.
1332 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1333 urlparse.urlparse(issue_arg))
1334 if not parsed_issue_arg or not parsed_issue_arg.valid:
1335 DieWithError('Failed to parse issue argument "%s". '
1336 'Must be an issue number or a valid URL.' % issue_arg)
1337 return self._codereview_impl.CMDPatchWithParsedIssue(
1338 parsed_issue_arg, reject, nocommit, directory)
1339
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001340 def CMDUpload(self, options, git_diff_args, orig_args):
1341 """Uploads a change to codereview."""
1342 if git_diff_args:
1343 # TODO(ukai): is it ok for gerrit case?
1344 base_branch = git_diff_args[0]
1345 else:
1346 if self.GetBranch() is None:
1347 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1348
1349 # Default to diffing against common ancestor of upstream branch
1350 base_branch = self.GetCommonAncestorWithUpstream()
1351 git_diff_args = [base_branch, 'HEAD']
1352
1353 # Make sure authenticated to codereview before running potentially expensive
1354 # hooks. It is a fast, best efforts check. Codereview still can reject the
1355 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001356 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001357
1358 # Apply watchlists on upload.
1359 change = self.GetChange(base_branch, None)
1360 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1361 files = [f.LocalPath() for f in change.AffectedFiles()]
1362 if not options.bypass_watchlists:
1363 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1364
1365 if not options.bypass_hooks:
1366 if options.reviewers or options.tbr_owners:
1367 # Set the reviewer list now so that presubmit checks can access it.
1368 change_description = ChangeDescription(change.FullDescriptionText())
1369 change_description.update_reviewers(options.reviewers,
1370 options.tbr_owners,
1371 change)
1372 change.SetDescriptionText(change_description.description)
1373 hook_results = self.RunHook(committing=False,
1374 may_prompt=not options.force,
1375 verbose=options.verbose,
1376 change=change)
1377 if not hook_results.should_continue():
1378 return 1
1379 if not options.reviewers and hook_results.reviewers:
1380 options.reviewers = hook_results.reviewers.split(',')
1381
1382 if self.GetIssue():
1383 latest_patchset = self.GetMostRecentPatchset()
1384 local_patchset = self.GetPatchset()
1385 if (latest_patchset and local_patchset and
1386 local_patchset != latest_patchset):
1387 print ('The last upload made from this repository was patchset #%d but '
1388 'the most recent patchset on the server is #%d.'
1389 % (local_patchset, latest_patchset))
1390 print ('Uploading will still work, but if you\'ve uploaded to this '
1391 'issue from another machine or branch the patch you\'re '
1392 'uploading now might not include those changes.')
1393 ask_for_data('About to upload; enter to confirm.')
1394
1395 print_stats(options.similarity, options.find_copies, git_diff_args)
1396 ret = self.CMDUploadChange(options, git_diff_args, change)
1397 if not ret:
1398 git_set_branch_value('last-upload-hash',
1399 RunGit(['rev-parse', 'HEAD']).strip())
1400 # Run post upload hooks, if specified.
1401 if settings.GetRunPostUploadHook():
1402 presubmit_support.DoPostUploadExecuter(
1403 change,
1404 self,
1405 settings.GetRoot(),
1406 options.verbose,
1407 sys.stdout)
1408
1409 # Upload all dependencies if specified.
1410 if options.dependencies:
1411 print
1412 print '--dependencies has been specified.'
1413 print 'All dependent local branches will be re-uploaded.'
1414 print
1415 # Remove the dependencies flag from args so that we do not end up in a
1416 # loop.
1417 orig_args.remove('--dependencies')
1418 ret = upload_branch_deps(self, orig_args)
1419 return ret
1420
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001421 def SetCQState(self, new_state):
1422 """Update the CQ state for latest patchset.
1423
1424 Issue must have been already uploaded and known.
1425 """
1426 assert new_state in _CQState.ALL_STATES
1427 assert self.GetIssue()
1428 return self._codereview_impl.SetCQState(new_state)
1429
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001430 # Forward methods to codereview specific implementation.
1431
1432 def CloseIssue(self):
1433 return self._codereview_impl.CloseIssue()
1434
1435 def GetStatus(self):
1436 return self._codereview_impl.GetStatus()
1437
1438 def GetCodereviewServer(self):
1439 return self._codereview_impl.GetCodereviewServer()
1440
1441 def GetApprovingReviewers(self):
1442 return self._codereview_impl.GetApprovingReviewers()
1443
1444 def GetMostRecentPatchset(self):
1445 return self._codereview_impl.GetMostRecentPatchset()
1446
1447 def __getattr__(self, attr):
1448 # This is because lots of untested code accesses Rietveld-specific stuff
1449 # directly, and it's hard to fix for sure. So, just let it work, and fix
1450 # on a cases by case basis.
1451 return getattr(self._codereview_impl, attr)
1452
1453
1454class _ChangelistCodereviewBase(object):
1455 """Abstract base class encapsulating codereview specifics of a changelist."""
1456 def __init__(self, changelist):
1457 self._changelist = changelist # instance of Changelist
1458
1459 def __getattr__(self, attr):
1460 # Forward methods to changelist.
1461 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1462 # _RietveldChangelistImpl to avoid this hack?
1463 return getattr(self._changelist, attr)
1464
1465 def GetStatus(self):
1466 """Apply a rough heuristic to give a simple summary of an issue's review
1467 or CQ status, assuming adherence to a common workflow.
1468
1469 Returns None if no issue for this branch, or specific string keywords.
1470 """
1471 raise NotImplementedError()
1472
1473 def GetCodereviewServer(self):
1474 """Returns server URL without end slash, like "https://codereview.com"."""
1475 raise NotImplementedError()
1476
1477 def FetchDescription(self):
1478 """Fetches and returns description from the codereview server."""
1479 raise NotImplementedError()
1480
1481 def GetCodereviewServerSetting(self):
1482 """Returns git config setting for the codereview server."""
1483 raise NotImplementedError()
1484
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001485 @classmethod
1486 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001487 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001488
1489 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001490 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001491 """Returns name of git config setting which stores issue number for a given
1492 branch."""
1493 raise NotImplementedError()
1494
1495 def PatchsetSetting(self):
1496 """Returns name of git config setting which stores issue number."""
1497 raise NotImplementedError()
1498
1499 def GetRieveldObjForPresubmit(self):
1500 # This is an unfortunate Rietveld-embeddedness in presubmit.
1501 # For non-Rietveld codereviews, this probably should return a dummy object.
1502 raise NotImplementedError()
1503
1504 def UpdateDescriptionRemote(self, description):
1505 """Update the description on codereview site."""
1506 raise NotImplementedError()
1507
1508 def CloseIssue(self):
1509 """Closes the issue."""
1510 raise NotImplementedError()
1511
1512 def GetApprovingReviewers(self):
1513 """Returns a list of reviewers approving the change.
1514
1515 Note: not necessarily committers.
1516 """
1517 raise NotImplementedError()
1518
1519 def GetMostRecentPatchset(self):
1520 """Returns the most recent patchset number from the codereview site."""
1521 raise NotImplementedError()
1522
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001523 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1524 directory):
1525 """Fetches and applies the issue.
1526
1527 Arguments:
1528 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1529 reject: if True, reject the failed patch instead of switching to 3-way
1530 merge. Rietveld only.
1531 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1532 only.
1533 directory: switch to directory before applying the patch. Rietveld only.
1534 """
1535 raise NotImplementedError()
1536
1537 @staticmethod
1538 def ParseIssueURL(parsed_url):
1539 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1540 failed."""
1541 raise NotImplementedError()
1542
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001543 def EnsureAuthenticated(self, force):
1544 """Best effort check that user is authenticated with codereview server.
1545
1546 Arguments:
1547 force: whether to skip confirmation questions.
1548 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001549 raise NotImplementedError()
1550
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001551 def CMDUploadChange(self, options, args, change):
1552 """Uploads a change to codereview."""
1553 raise NotImplementedError()
1554
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001555 def SetCQState(self, new_state):
1556 """Update the CQ state for latest patchset.
1557
1558 Issue must have been already uploaded and known.
1559 """
1560 raise NotImplementedError()
1561
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001562
1563class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1564 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1565 super(_RietveldChangelistImpl, self).__init__(changelist)
1566 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1567 settings.GetDefaultServerUrl()
1568
1569 self._rietveld_server = rietveld_server
1570 self._auth_config = auth_config
1571 self._props = None
1572 self._rpc_server = None
1573
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001574 def GetCodereviewServer(self):
1575 if not self._rietveld_server:
1576 # If we're on a branch then get the server potentially associated
1577 # with that branch.
1578 if self.GetIssue():
1579 rietveld_server_setting = self.GetCodereviewServerSetting()
1580 if rietveld_server_setting:
1581 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1582 ['config', rietveld_server_setting], error_ok=True).strip())
1583 if not self._rietveld_server:
1584 self._rietveld_server = settings.GetDefaultServerUrl()
1585 return self._rietveld_server
1586
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001587 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 """Best effort check that user is authenticated with Rietveld server."""
1589 if self._auth_config.use_oauth2:
1590 authenticator = auth.get_authenticator_for_host(
1591 self.GetCodereviewServer(), self._auth_config)
1592 if not authenticator.has_cached_credentials():
1593 raise auth.LoginRequiredError(self.GetCodereviewServer())
1594
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001595 def FetchDescription(self):
1596 issue = self.GetIssue()
1597 assert issue
1598 try:
1599 return self.RpcServer().get_description(issue).strip()
1600 except urllib2.HTTPError as e:
1601 if e.code == 404:
1602 DieWithError(
1603 ('\nWhile fetching the description for issue %d, received a '
1604 '404 (not found)\n'
1605 'error. It is likely that you deleted this '
1606 'issue on the server. If this is the\n'
1607 'case, please run\n\n'
1608 ' git cl issue 0\n\n'
1609 'to clear the association with the deleted issue. Then run '
1610 'this command again.') % issue)
1611 else:
1612 DieWithError(
1613 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1614 except urllib2.URLError as e:
1615 print >> sys.stderr, (
1616 'Warning: Failed to retrieve CL description due to network '
1617 'failure.')
1618 return ''
1619
1620 def GetMostRecentPatchset(self):
1621 return self.GetIssueProperties()['patchsets'][-1]
1622
1623 def GetPatchSetDiff(self, issue, patchset):
1624 return self.RpcServer().get(
1625 '/download/issue%s_%s.diff' % (issue, patchset))
1626
1627 def GetIssueProperties(self):
1628 if self._props is None:
1629 issue = self.GetIssue()
1630 if not issue:
1631 self._props = {}
1632 else:
1633 self._props = self.RpcServer().get_issue_properties(issue, True)
1634 return self._props
1635
1636 def GetApprovingReviewers(self):
1637 return get_approving_reviewers(self.GetIssueProperties())
1638
1639 def AddComment(self, message):
1640 return self.RpcServer().add_comment(self.GetIssue(), message)
1641
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001642 def GetStatus(self):
1643 """Apply a rough heuristic to give a simple summary of an issue's review
1644 or CQ status, assuming adherence to a common workflow.
1645
1646 Returns None if no issue for this branch, or one of the following keywords:
1647 * 'error' - error from review tool (including deleted issues)
1648 * 'unsent' - not sent for review
1649 * 'waiting' - waiting for review
1650 * 'reply' - waiting for owner to reply to review
1651 * 'lgtm' - LGTM from at least one approved reviewer
1652 * 'commit' - in the commit queue
1653 * 'closed' - closed
1654 """
1655 if not self.GetIssue():
1656 return None
1657
1658 try:
1659 props = self.GetIssueProperties()
1660 except urllib2.HTTPError:
1661 return 'error'
1662
1663 if props.get('closed'):
1664 # Issue is closed.
1665 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001666 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001667 # Issue is in the commit queue.
1668 return 'commit'
1669
1670 try:
1671 reviewers = self.GetApprovingReviewers()
1672 except urllib2.HTTPError:
1673 return 'error'
1674
1675 if reviewers:
1676 # Was LGTM'ed.
1677 return 'lgtm'
1678
1679 messages = props.get('messages') or []
1680
1681 if not messages:
1682 # No message was sent.
1683 return 'unsent'
1684 if messages[-1]['sender'] != props.get('owner_email'):
1685 # Non-LGTM reply from non-owner
1686 return 'reply'
1687 return 'waiting'
1688
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001690 return self.RpcServer().update_description(
1691 self.GetIssue(), self.description)
1692
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001693 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001694 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001695
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001696 def SetFlag(self, flag, value):
1697 """Patchset must match."""
1698 if not self.GetPatchset():
1699 DieWithError('The patchset needs to match. Send another patchset.')
1700 try:
1701 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001702 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001703 except urllib2.HTTPError, e:
1704 if e.code == 404:
1705 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1706 if e.code == 403:
1707 DieWithError(
1708 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1709 'match?') % (self.GetIssue(), self.GetPatchset()))
1710 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001711
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001712 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713 """Returns an upload.RpcServer() to access this review's rietveld instance.
1714 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001715 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001716 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001718 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001719 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001721 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001722 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001723 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 """Return the git setting that stores this change's most recent patchset."""
1727 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1728
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001729 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001731 branch = self.GetBranch()
1732 if branch:
1733 return 'branch.%s.rietveldserver' % branch
1734 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001736 def GetRieveldObjForPresubmit(self):
1737 return self.RpcServer()
1738
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001739 def SetCQState(self, new_state):
1740 props = self.GetIssueProperties()
1741 if props.get('private'):
1742 DieWithError('Cannot set-commit on private issue')
1743
1744 if new_state == _CQState.COMMIT:
1745 self.SetFlag('commit', '1')
1746 elif new_state == _CQState.NONE:
1747 self.SetFlag('commit', '0')
1748 else:
1749 raise NotImplementedError()
1750
1751
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001752 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1753 directory):
1754 # TODO(maruel): Use apply_issue.py
1755
1756 # PatchIssue should never be called with a dirty tree. It is up to the
1757 # caller to check this, but just in case we assert here since the
1758 # consequences of the caller not checking this could be dire.
1759 assert(not git_common.is_dirty_git_tree('apply'))
1760 assert(parsed_issue_arg.valid)
1761 self._changelist.issue = parsed_issue_arg.issue
1762 if parsed_issue_arg.hostname:
1763 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1764
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001765 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1766 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001767 assert parsed_issue_arg.patchset
1768 patchset = parsed_issue_arg.patchset
1769 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1770 else:
1771 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1772 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1773
1774 # Switch up to the top-level directory, if necessary, in preparation for
1775 # applying the patch.
1776 top = settings.GetRelativeRoot()
1777 if top:
1778 os.chdir(top)
1779
1780 # Git patches have a/ at the beginning of source paths. We strip that out
1781 # with a sed script rather than the -p flag to patch so we can feed either
1782 # Git or svn-style patches into the same apply command.
1783 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1784 try:
1785 patch_data = subprocess2.check_output(
1786 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1787 except subprocess2.CalledProcessError:
1788 DieWithError('Git patch mungling failed.')
1789 logging.info(patch_data)
1790
1791 # We use "git apply" to apply the patch instead of "patch" so that we can
1792 # pick up file adds.
1793 # The --index flag means: also insert into the index (so we catch adds).
1794 cmd = ['git', 'apply', '--index', '-p0']
1795 if directory:
1796 cmd.extend(('--directory', directory))
1797 if reject:
1798 cmd.append('--reject')
1799 elif IsGitVersionAtLeast('1.7.12'):
1800 cmd.append('--3way')
1801 try:
1802 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1803 stdin=patch_data, stdout=subprocess2.VOID)
1804 except subprocess2.CalledProcessError:
1805 print 'Failed to apply the patch'
1806 return 1
1807
1808 # If we had an issue, commit the current state and register the issue.
1809 if not nocommit:
1810 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1811 'patch from issue %(i)s at patchset '
1812 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1813 % {'i': self.GetIssue(), 'p': patchset})])
1814 self.SetIssue(self.GetIssue())
1815 self.SetPatchset(patchset)
1816 print "Committed patch locally."
1817 else:
1818 print "Patch applied to index."
1819 return 0
1820
1821 @staticmethod
1822 def ParseIssueURL(parsed_url):
1823 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1824 return None
1825 # Typical url: https://domain/<issue_number>[/[other]]
1826 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1827 if match:
1828 return _RietveldParsedIssueNumberArgument(
1829 issue=int(match.group(1)),
1830 hostname=parsed_url.netloc)
1831 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1832 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1833 if match:
1834 return _RietveldParsedIssueNumberArgument(
1835 issue=int(match.group(1)),
1836 patchset=int(match.group(2)),
1837 hostname=parsed_url.netloc,
1838 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1839 return None
1840
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001841 def CMDUploadChange(self, options, args, change):
1842 """Upload the patch to Rietveld."""
1843 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1844 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001845 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1846 if options.emulate_svn_auto_props:
1847 upload_args.append('--emulate_svn_auto_props')
1848
1849 change_desc = None
1850
1851 if options.email is not None:
1852 upload_args.extend(['--email', options.email])
1853
1854 if self.GetIssue():
1855 if options.title:
1856 upload_args.extend(['--title', options.title])
1857 if options.message:
1858 upload_args.extend(['--message', options.message])
1859 upload_args.extend(['--issue', str(self.GetIssue())])
1860 print ('This branch is associated with issue %s. '
1861 'Adding patch to that issue.' % self.GetIssue())
1862 else:
1863 if options.title:
1864 upload_args.extend(['--title', options.title])
1865 message = (options.title or options.message or
1866 CreateDescriptionFromLog(args))
1867 change_desc = ChangeDescription(message)
1868 if options.reviewers or options.tbr_owners:
1869 change_desc.update_reviewers(options.reviewers,
1870 options.tbr_owners,
1871 change)
1872 if not options.force:
1873 change_desc.prompt()
1874
1875 if not change_desc.description:
1876 print "Description is empty; aborting."
1877 return 1
1878
1879 upload_args.extend(['--message', change_desc.description])
1880 if change_desc.get_reviewers():
1881 upload_args.append('--reviewers=%s' % ','.join(
1882 change_desc.get_reviewers()))
1883 if options.send_mail:
1884 if not change_desc.get_reviewers():
1885 DieWithError("Must specify reviewers to send email.")
1886 upload_args.append('--send_mail')
1887
1888 # We check this before applying rietveld.private assuming that in
1889 # rietveld.cc only addresses which we can send private CLs to are listed
1890 # if rietveld.private is set, and so we should ignore rietveld.cc only
1891 # when --private is specified explicitly on the command line.
1892 if options.private:
1893 logging.warn('rietveld.cc is ignored since private flag is specified. '
1894 'You need to review and add them manually if necessary.')
1895 cc = self.GetCCListWithoutDefault()
1896 else:
1897 cc = self.GetCCList()
1898 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1899 if cc:
1900 upload_args.extend(['--cc', cc])
1901
1902 if options.private or settings.GetDefaultPrivateFlag() == "True":
1903 upload_args.append('--private')
1904
1905 upload_args.extend(['--git_similarity', str(options.similarity)])
1906 if not options.find_copies:
1907 upload_args.extend(['--git_no_find_copies'])
1908
1909 # Include the upstream repo's URL in the change -- this is useful for
1910 # projects that have their source spread across multiple repos.
1911 remote_url = self.GetGitBaseUrlFromConfig()
1912 if not remote_url:
1913 if settings.GetIsGitSvn():
1914 remote_url = self.GetGitSvnRemoteUrl()
1915 else:
1916 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1917 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1918 self.GetUpstreamBranch().split('/')[-1])
1919 if remote_url:
1920 upload_args.extend(['--base_url', remote_url])
1921 remote, remote_branch = self.GetRemoteBranch()
1922 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1923 settings.GetPendingRefPrefix())
1924 if target_ref:
1925 upload_args.extend(['--target_ref', target_ref])
1926
1927 # Look for dependent patchsets. See crbug.com/480453 for more details.
1928 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1929 upstream_branch = ShortBranchName(upstream_branch)
1930 if remote is '.':
1931 # A local branch is being tracked.
1932 local_branch = ShortBranchName(upstream_branch)
1933 if settings.GetIsSkipDependencyUpload(local_branch):
1934 print
1935 print ('Skipping dependency patchset upload because git config '
1936 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1937 print
1938 else:
1939 auth_config = auth.extract_auth_config_from_options(options)
1940 branch_cl = Changelist(branchref=local_branch,
1941 auth_config=auth_config)
1942 branch_cl_issue_url = branch_cl.GetIssueURL()
1943 branch_cl_issue = branch_cl.GetIssue()
1944 branch_cl_patchset = branch_cl.GetPatchset()
1945 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1946 upload_args.extend(
1947 ['--depends_on_patchset', '%s:%s' % (
1948 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001949 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001950 '\n'
1951 'The current branch (%s) is tracking a local branch (%s) with '
1952 'an associated CL.\n'
1953 'Adding %s/#ps%s as a dependency patchset.\n'
1954 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1955 branch_cl_patchset))
1956
1957 project = settings.GetProject()
1958 if project:
1959 upload_args.extend(['--project', project])
1960
1961 if options.cq_dry_run:
1962 upload_args.extend(['--cq_dry_run'])
1963
1964 try:
1965 upload_args = ['upload'] + upload_args + args
1966 logging.info('upload.RealMain(%s)', upload_args)
1967 issue, patchset = upload.RealMain(upload_args)
1968 issue = int(issue)
1969 patchset = int(patchset)
1970 except KeyboardInterrupt:
1971 sys.exit(1)
1972 except:
1973 # If we got an exception after the user typed a description for their
1974 # change, back up the description before re-raising.
1975 if change_desc:
1976 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1977 print('\nGot exception while uploading -- saving description to %s\n' %
1978 backup_path)
1979 backup_file = open(backup_path, 'w')
1980 backup_file.write(change_desc.description)
1981 backup_file.close()
1982 raise
1983
1984 if not self.GetIssue():
1985 self.SetIssue(issue)
1986 self.SetPatchset(patchset)
1987
1988 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001989 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001990 return 0
1991
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001992
1993class _GerritChangelistImpl(_ChangelistCodereviewBase):
1994 def __init__(self, changelist, auth_config=None):
1995 # auth_config is Rietveld thing, kept here to preserve interface only.
1996 super(_GerritChangelistImpl, self).__init__(changelist)
1997 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001998 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001999 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002000 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002001
2002 def _GetGerritHost(self):
2003 # Lazy load of configs.
2004 self.GetCodereviewServer()
2005 return self._gerrit_host
2006
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002007 def _GetGitHost(self):
2008 """Returns git host to be used when uploading change to Gerrit."""
2009 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2010
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002011 def GetCodereviewServer(self):
2012 if not self._gerrit_server:
2013 # If we're on a branch then get the server potentially associated
2014 # with that branch.
2015 if self.GetIssue():
2016 gerrit_server_setting = self.GetCodereviewServerSetting()
2017 if gerrit_server_setting:
2018 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2019 error_ok=True).strip()
2020 if self._gerrit_server:
2021 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2022 if not self._gerrit_server:
2023 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2024 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002025 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002026 parts[0] = parts[0] + '-review'
2027 self._gerrit_host = '.'.join(parts)
2028 self._gerrit_server = 'https://%s' % self._gerrit_host
2029 return self._gerrit_server
2030
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002031 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002032 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002033 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002034
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002035 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002036 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002037 if settings.GetGerritSkipEnsureAuthenticated():
2038 # For projects with unusual authentication schemes.
2039 # See http://crbug.com/603378.
2040 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002041 # Lazy-loader to identify Gerrit and Git hosts.
2042 if gerrit_util.GceAuthenticator.is_gce():
2043 return
2044 self.GetCodereviewServer()
2045 git_host = self._GetGitHost()
2046 assert self._gerrit_server and self._gerrit_host
2047 cookie_auth = gerrit_util.CookiesAuthenticator()
2048
2049 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2050 git_auth = cookie_auth.get_auth_header(git_host)
2051 if gerrit_auth and git_auth:
2052 if gerrit_auth == git_auth:
2053 return
2054 print((
2055 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2056 ' Check your %s or %s file for credentials of hosts:\n'
2057 ' %s\n'
2058 ' %s\n'
2059 ' %s') %
2060 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2061 git_host, self._gerrit_host,
2062 cookie_auth.get_new_password_message(git_host)))
2063 if not force:
2064 ask_for_data('If you know what you are doing, press Enter to continue, '
2065 'Ctrl+C to abort.')
2066 return
2067 else:
2068 missing = (
2069 [] if gerrit_auth else [self._gerrit_host] +
2070 [] if git_auth else [git_host])
2071 DieWithError('Credentials for the following hosts are required:\n'
2072 ' %s\n'
2073 'These are read from %s (or legacy %s)\n'
2074 '%s' % (
2075 '\n '.join(missing),
2076 cookie_auth.get_gitcookies_path(),
2077 cookie_auth.get_netrc_path(),
2078 cookie_auth.get_new_password_message(git_host)))
2079
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002080
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002081 def PatchsetSetting(self):
2082 """Return the git setting that stores this change's most recent patchset."""
2083 return 'branch.%s.gerritpatchset' % self.GetBranch()
2084
2085 def GetCodereviewServerSetting(self):
2086 """Returns the git setting that stores this change's Gerrit server."""
2087 branch = self.GetBranch()
2088 if branch:
2089 return 'branch.%s.gerritserver' % branch
2090 return None
2091
2092 def GetRieveldObjForPresubmit(self):
2093 class ThisIsNotRietveldIssue(object):
2094 def __nonzero__(self):
2095 # This is a hack to make presubmit_support think that rietveld is not
2096 # defined, yet still ensure that calls directly result in a decent
2097 # exception message below.
2098 return False
2099
2100 def __getattr__(self, attr):
2101 print(
2102 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2103 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2104 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2105 'or use Rietveld for codereview.\n'
2106 'See also http://crbug.com/579160.' % attr)
2107 raise NotImplementedError()
2108 return ThisIsNotRietveldIssue()
2109
2110 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002111 """Apply a rough heuristic to give a simple summary of an issue's review
2112 or CQ status, assuming adherence to a common workflow.
2113
2114 Returns None if no issue for this branch, or one of the following keywords:
2115 * 'error' - error from review tool (including deleted issues)
2116 * 'unsent' - no reviewers added
2117 * 'waiting' - waiting for review
2118 * 'reply' - waiting for owner to reply to review
2119 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2120 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2121 * 'commit' - in the commit queue
2122 * 'closed' - abandoned
2123 """
2124 if not self.GetIssue():
2125 return None
2126
2127 try:
2128 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2129 except httplib.HTTPException:
2130 return 'error'
2131
2132 if data['status'] == 'ABANDONED':
2133 return 'closed'
2134
2135 cq_label = data['labels'].get('Commit-Queue', {})
2136 if cq_label:
2137 # Vote value is a stringified integer, which we expect from 0 to 2.
2138 vote_value = cq_label.get('value', '0')
2139 vote_text = cq_label.get('values', {}).get(vote_value, '')
2140 if vote_text.lower() == 'commit':
2141 return 'commit'
2142
2143 lgtm_label = data['labels'].get('Code-Review', {})
2144 if lgtm_label:
2145 if 'rejected' in lgtm_label:
2146 return 'not lgtm'
2147 if 'approved' in lgtm_label:
2148 return 'lgtm'
2149
2150 if not data.get('reviewers', {}).get('REVIEWER', []):
2151 return 'unsent'
2152
2153 messages = data.get('messages', [])
2154 if messages:
2155 owner = data['owner'].get('_account_id')
2156 last_message_author = messages[-1].get('author', {}).get('_account_id')
2157 if owner != last_message_author:
2158 # Some reply from non-owner.
2159 return 'reply'
2160
2161 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002162
2163 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002164 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002165 return data['revisions'][data['current_revision']]['_number']
2166
2167 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002168 data = self._GetChangeDetail(['CURRENT_REVISION'])
2169 current_rev = data['current_revision']
2170 url = data['revisions'][current_rev]['fetch']['http']['url']
2171 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002172
2173 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002174 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2175 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002176
2177 def CloseIssue(self):
2178 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2179
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002180 def GetApprovingReviewers(self):
2181 """Returns a list of reviewers approving the change.
2182
2183 Note: not necessarily committers.
2184 """
2185 raise NotImplementedError()
2186
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002187 def SubmitIssue(self, wait_for_merge=True):
2188 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2189 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191 def _GetChangeDetail(self, options=None, issue=None):
2192 options = options or []
2193 issue = issue or self.GetIssue()
2194 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002195 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2196 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002197
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002198 def CMDLand(self, force, bypass_hooks, verbose):
2199 if git_common.is_dirty_git_tree('land'):
2200 return 1
2201 differs = True
2202 last_upload = RunGit(['config',
2203 'branch.%s.gerritsquashhash' % self.GetBranch()],
2204 error_ok=True).strip()
2205 # Note: git diff outputs nothing if there is no diff.
2206 if not last_upload or RunGit(['diff', last_upload]).strip():
2207 print('WARNING: some changes from local branch haven\'t been uploaded')
2208 else:
2209 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2210 if detail['current_revision'] == last_upload:
2211 differs = False
2212 else:
2213 print('WARNING: local branch contents differ from latest uploaded '
2214 'patchset')
2215 if differs:
2216 if not force:
2217 ask_for_data(
2218 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2219 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2220 elif not bypass_hooks:
2221 hook_results = self.RunHook(
2222 committing=True,
2223 may_prompt=not force,
2224 verbose=verbose,
2225 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2226 if not hook_results.should_continue():
2227 return 1
2228
2229 self.SubmitIssue(wait_for_merge=True)
2230 print('Issue %s has been submitted.' % self.GetIssueURL())
2231 return 0
2232
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002233 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2234 directory):
2235 assert not reject
2236 assert not nocommit
2237 assert not directory
2238 assert parsed_issue_arg.valid
2239
2240 self._changelist.issue = parsed_issue_arg.issue
2241
2242 if parsed_issue_arg.hostname:
2243 self._gerrit_host = parsed_issue_arg.hostname
2244 self._gerrit_server = 'https://%s' % self._gerrit_host
2245
2246 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2247
2248 if not parsed_issue_arg.patchset:
2249 # Use current revision by default.
2250 revision_info = detail['revisions'][detail['current_revision']]
2251 patchset = int(revision_info['_number'])
2252 else:
2253 patchset = parsed_issue_arg.patchset
2254 for revision_info in detail['revisions'].itervalues():
2255 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2256 break
2257 else:
2258 DieWithError('Couldn\'t find patchset %i in issue %i' %
2259 (parsed_issue_arg.patchset, self.GetIssue()))
2260
2261 fetch_info = revision_info['fetch']['http']
2262 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2263 RunGit(['cherry-pick', 'FETCH_HEAD'])
2264 self.SetIssue(self.GetIssue())
2265 self.SetPatchset(patchset)
2266 print('Committed patch for issue %i pathset %i locally' %
2267 (self.GetIssue(), self.GetPatchset()))
2268 return 0
2269
2270 @staticmethod
2271 def ParseIssueURL(parsed_url):
2272 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2273 return None
2274 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2275 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2276 # Short urls like https://domain/<issue_number> can be used, but don't allow
2277 # specifying the patchset (you'd 404), but we allow that here.
2278 if parsed_url.path == '/':
2279 part = parsed_url.fragment
2280 else:
2281 part = parsed_url.path
2282 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2283 if match:
2284 return _ParsedIssueNumberArgument(
2285 issue=int(match.group(2)),
2286 patchset=int(match.group(4)) if match.group(4) else None,
2287 hostname=parsed_url.netloc)
2288 return None
2289
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002290 def CMDUploadChange(self, options, args, change):
2291 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002292 if options.squash and options.no_squash:
2293 DieWithError('Can only use one of --squash or --no-squash')
2294 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2295 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002296 # We assume the remote called "origin" is the one we want.
2297 # It is probably not worthwhile to support different workflows.
2298 gerrit_remote = 'origin'
2299
2300 remote, remote_branch = self.GetRemoteBranch()
2301 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2302 pending_prefix='')
2303
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002304 if options.squash:
2305 if not self.GetIssue():
2306 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2307 # with shadow branch, which used to contain change-id for a given
2308 # branch, using which we can fetch actual issue number and set it as the
2309 # property of the branch, which is the new way.
2310 message = RunGitSilent([
2311 'show', '--format=%B', '-s',
2312 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2313 if message:
2314 change_ids = git_footers.get_footer_change_id(message.strip())
2315 if change_ids and len(change_ids) == 1:
2316 details = self._GetChangeDetail(issue=change_ids[0])
2317 if details:
2318 print('WARNING: found old upload in branch git_cl_uploads/%s '
2319 'corresponding to issue %s' %
2320 (self.GetBranch(), details['_number']))
2321 self.SetIssue(details['_number'])
2322 if not self.GetIssue():
2323 DieWithError(
2324 '\n' # For readability of the blob below.
2325 'Found old upload in branch git_cl_uploads/%s, '
2326 'but failed to find corresponding Gerrit issue.\n'
2327 'If you know the issue number, set it manually first:\n'
2328 ' git cl issue 123456\n'
2329 'If you intended to upload this CL as new issue, '
2330 'just delete or rename the old upload branch:\n'
2331 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2332 'After that, please run git cl upload again.' %
2333 tuple([self.GetBranch()] * 3))
2334 # End of backwards compatability.
2335
2336 if self.GetIssue():
2337 # Try to get the message from a previous upload.
2338 message = self.GetDescription()
2339 if not message:
2340 DieWithError(
2341 'failed to fetch description from current Gerrit issue %d\n'
2342 '%s' % (self.GetIssue(), self.GetIssueURL()))
2343 change_id = self._GetChangeDetail()['change_id']
2344 while True:
2345 footer_change_ids = git_footers.get_footer_change_id(message)
2346 if footer_change_ids == [change_id]:
2347 break
2348 if not footer_change_ids:
2349 message = git_footers.add_footer_change_id(message, change_id)
2350 print('WARNING: appended missing Change-Id to issue description')
2351 continue
2352 # There is already a valid footer but with different or several ids.
2353 # Doing this automatically is non-trivial as we don't want to lose
2354 # existing other footers, yet we want to append just 1 desired
2355 # Change-Id. Thus, just create a new footer, but let user verify the
2356 # new description.
2357 message = '%s\n\nChange-Id: %s' % (message, change_id)
2358 print(
2359 'WARNING: issue %s has Change-Id footer(s):\n'
2360 ' %s\n'
2361 'but issue has Change-Id %s, according to Gerrit.\n'
2362 'Please, check the proposed correction to the description, '
2363 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2364 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2365 change_id))
2366 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2367 if not options.force:
2368 change_desc = ChangeDescription(message)
2369 change_desc.prompt()
2370 message = change_desc.description
2371 if not message:
2372 DieWithError("Description is empty. Aborting...")
2373 # Continue the while loop.
2374 # Sanity check of this code - we should end up with proper message
2375 # footer.
2376 assert [change_id] == git_footers.get_footer_change_id(message)
2377 change_desc = ChangeDescription(message)
2378 else:
2379 change_desc = ChangeDescription(
2380 options.message or CreateDescriptionFromLog(args))
2381 if not options.force:
2382 change_desc.prompt()
2383 if not change_desc.description:
2384 DieWithError("Description is empty. Aborting...")
2385 message = change_desc.description
2386 change_ids = git_footers.get_footer_change_id(message)
2387 if len(change_ids) > 1:
2388 DieWithError('too many Change-Id footers, at most 1 allowed.')
2389 if not change_ids:
2390 # Generate the Change-Id automatically.
2391 message = git_footers.add_footer_change_id(
2392 message, GenerateGerritChangeId(message))
2393 change_desc.set_description(message)
2394 change_ids = git_footers.get_footer_change_id(message)
2395 assert len(change_ids) == 1
2396 change_id = change_ids[0]
2397
2398 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2399 if remote is '.':
2400 # If our upstream branch is local, we base our squashed commit on its
2401 # squashed version.
2402 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2403 # Check the squashed hash of the parent.
2404 parent = RunGit(['config',
2405 'branch.%s.gerritsquashhash' % upstream_branch_name],
2406 error_ok=True).strip()
2407 # Verify that the upstream branch has been uploaded too, otherwise
2408 # Gerrit will create additional CLs when uploading.
2409 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2410 RunGitSilent(['rev-parse', parent + ':'])):
2411 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2412 DieWithError(
2413 'Upload upstream branch %s first.\n'
2414 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2415 'version of depot_tools. If so, then re-upload it with:\n'
2416 ' git cl upload --squash\n' % upstream_branch_name)
2417 else:
2418 parent = self.GetCommonAncestorWithUpstream()
2419
2420 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2421 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2422 '-m', message]).strip()
2423 else:
2424 change_desc = ChangeDescription(
2425 options.message or CreateDescriptionFromLog(args))
2426 if not change_desc.description:
2427 DieWithError("Description is empty. Aborting...")
2428
2429 if not git_footers.get_footer_change_id(change_desc.description):
2430 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002431 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2432 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002433 ref_to_push = 'HEAD'
2434 parent = '%s/%s' % (gerrit_remote, branch)
2435 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2436
2437 assert change_desc
2438 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2439 ref_to_push)]).splitlines()
2440 if len(commits) > 1:
2441 print('WARNING: This will upload %d commits. Run the following command '
2442 'to see which commits will be uploaded: ' % len(commits))
2443 print('git log %s..%s' % (parent, ref_to_push))
2444 print('You can also use `git squash-branch` to squash these into a '
2445 'single commit.')
2446 ask_for_data('About to upload; enter to confirm.')
2447
2448 if options.reviewers or options.tbr_owners:
2449 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2450 change)
2451
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002452 # Extra options that can be specified at push time. Doc:
2453 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2454 refspec_opts = []
2455 if options.title:
2456 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2457 # reverse on its side.
2458 if '_' in options.title:
2459 print('WARNING: underscores in title will be converted to spaces.')
2460 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2461
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002462 cc = self.GetCCList().split(',')
2463 if options.cc:
2464 cc.extend(options.cc)
2465 cc = filter(None, cc)
2466 if cc:
tandrii@chromium.org0b2d7072016-04-18 16:19:03 +00002467 # refspec_opts.extend('cc=' + email.strip() for email in cc)
2468 # TODO(tandrii): enable this back. http://crbug.com/604377
2469 print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2470 ' Ignoring cc-ed emails. See http://crbug.com/604377.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002471
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002472 if change_desc.get_reviewers():
2473 refspec_opts.extend('r=' + email.strip()
2474 for email in change_desc.get_reviewers())
2475
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002476
2477 refspec_suffix = ''
2478 if refspec_opts:
2479 refspec_suffix = '%' + ','.join(refspec_opts)
2480 assert ' ' not in refspec_suffix, (
2481 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002482 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002483
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002484 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002485 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002486 print_stdout=True,
2487 # Flush after every line: useful for seeing progress when running as
2488 # recipe.
2489 filter_fn=lambda _: sys.stdout.flush())
2490
2491 if options.squash:
2492 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2493 change_numbers = [m.group(1)
2494 for m in map(regex.match, push_stdout.splitlines())
2495 if m]
2496 if len(change_numbers) != 1:
2497 DieWithError(
2498 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2499 'Change-Id: %s') % (len(change_numbers), change_id))
2500 self.SetIssue(change_numbers[0])
2501 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2502 ref_to_push])
2503 return 0
2504
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002505 def _AddChangeIdToCommitMessage(self, options, args):
2506 """Re-commits using the current message, assumes the commit hook is in
2507 place.
2508 """
2509 log_desc = options.message or CreateDescriptionFromLog(args)
2510 git_command = ['commit', '--amend', '-m', log_desc]
2511 RunGit(git_command)
2512 new_log_desc = CreateDescriptionFromLog(args)
2513 if git_footers.get_footer_change_id(new_log_desc):
2514 print 'git-cl: Added Change-Id to commit message.'
2515 return new_log_desc
2516 else:
2517 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002518
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002519 def SetCQState(self, new_state):
2520 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2521 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2522 # self-discovery of label config for this CL using REST API.
2523 vote_map = {
2524 _CQState.NONE: 0,
2525 _CQState.DRY_RUN: 1,
2526 _CQState.COMMIT : 2,
2527 }
2528 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2529 labels={'Commit-Queue': vote_map[new_state]})
2530
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002531
2532_CODEREVIEW_IMPLEMENTATIONS = {
2533 'rietveld': _RietveldChangelistImpl,
2534 'gerrit': _GerritChangelistImpl,
2535}
2536
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002537
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002538def _add_codereview_select_options(parser):
2539 """Appends --gerrit and --rietveld options to force specific codereview."""
2540 parser.codereview_group = optparse.OptionGroup(
2541 parser, 'EXPERIMENTAL! Codereview override options')
2542 parser.add_option_group(parser.codereview_group)
2543 parser.codereview_group.add_option(
2544 '--gerrit', action='store_true',
2545 help='Force the use of Gerrit for codereview')
2546 parser.codereview_group.add_option(
2547 '--rietveld', action='store_true',
2548 help='Force the use of Rietveld for codereview')
2549
2550
2551def _process_codereview_select_options(parser, options):
2552 if options.gerrit and options.rietveld:
2553 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2554 options.forced_codereview = None
2555 if options.gerrit:
2556 options.forced_codereview = 'gerrit'
2557 elif options.rietveld:
2558 options.forced_codereview = 'rietveld'
2559
2560
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002561class ChangeDescription(object):
2562 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002563 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002564 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002565
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002566 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002567 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002568
agable@chromium.org42c20792013-09-12 17:34:49 +00002569 @property # www.logilab.org/ticket/89786
2570 def description(self): # pylint: disable=E0202
2571 return '\n'.join(self._description_lines)
2572
2573 def set_description(self, desc):
2574 if isinstance(desc, basestring):
2575 lines = desc.splitlines()
2576 else:
2577 lines = [line.rstrip() for line in desc]
2578 while lines and not lines[0]:
2579 lines.pop(0)
2580 while lines and not lines[-1]:
2581 lines.pop(-1)
2582 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002583
piman@chromium.org336f9122014-09-04 02:16:55 +00002584 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002585 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002586 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002587 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002588 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002589 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002590
agable@chromium.org42c20792013-09-12 17:34:49 +00002591 # Get the set of R= and TBR= lines and remove them from the desciption.
2592 regexp = re.compile(self.R_LINE)
2593 matches = [regexp.match(line) for line in self._description_lines]
2594 new_desc = [l for i, l in enumerate(self._description_lines)
2595 if not matches[i]]
2596 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002597
agable@chromium.org42c20792013-09-12 17:34:49 +00002598 # Construct new unified R= and TBR= lines.
2599 r_names = []
2600 tbr_names = []
2601 for match in matches:
2602 if not match:
2603 continue
2604 people = cleanup_list([match.group(2).strip()])
2605 if match.group(1) == 'TBR':
2606 tbr_names.extend(people)
2607 else:
2608 r_names.extend(people)
2609 for name in r_names:
2610 if name not in reviewers:
2611 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002612 if add_owners_tbr:
2613 owners_db = owners.Database(change.RepositoryRoot(),
2614 fopen=file, os_path=os.path, glob=glob.glob)
2615 all_reviewers = set(tbr_names + reviewers)
2616 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2617 all_reviewers)
2618 tbr_names.extend(owners_db.reviewers_for(missing_files,
2619 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002620 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2621 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2622
2623 # Put the new lines in the description where the old first R= line was.
2624 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2625 if 0 <= line_loc < len(self._description_lines):
2626 if new_tbr_line:
2627 self._description_lines.insert(line_loc, new_tbr_line)
2628 if new_r_line:
2629 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002630 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002631 if new_r_line:
2632 self.append_footer(new_r_line)
2633 if new_tbr_line:
2634 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002635
2636 def prompt(self):
2637 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002638 self.set_description([
2639 '# Enter a description of the change.',
2640 '# This will be displayed on the codereview site.',
2641 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002642 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002643 '--------------------',
2644 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002645
agable@chromium.org42c20792013-09-12 17:34:49 +00002646 regexp = re.compile(self.BUG_LINE)
2647 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002648 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002649 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002650 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002651 if not content:
2652 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002653 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002654
2655 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002656 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2657 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002658 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002659 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002660
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002661 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002662 if self._description_lines:
2663 # Add an empty line if either the last line or the new line isn't a tag.
2664 last_line = self._description_lines[-1]
2665 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2666 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2667 self._description_lines.append('')
2668 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002669
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002670 def get_reviewers(self):
2671 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002672 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2673 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002674 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002675
2676
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002677def get_approving_reviewers(props):
2678 """Retrieves the reviewers that approved a CL from the issue properties with
2679 messages.
2680
2681 Note that the list may contain reviewers that are not committer, thus are not
2682 considered by the CQ.
2683 """
2684 return sorted(
2685 set(
2686 message['sender']
2687 for message in props['messages']
2688 if message['approval'] and message['sender'] in props['reviewers']
2689 )
2690 )
2691
2692
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002693def FindCodereviewSettingsFile(filename='codereview.settings'):
2694 """Finds the given file starting in the cwd and going up.
2695
2696 Only looks up to the top of the repository unless an
2697 'inherit-review-settings-ok' file exists in the root of the repository.
2698 """
2699 inherit_ok_file = 'inherit-review-settings-ok'
2700 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002701 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002702 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2703 root = '/'
2704 while True:
2705 if filename in os.listdir(cwd):
2706 if os.path.isfile(os.path.join(cwd, filename)):
2707 return open(os.path.join(cwd, filename))
2708 if cwd == root:
2709 break
2710 cwd = os.path.dirname(cwd)
2711
2712
2713def LoadCodereviewSettingsFromFile(fileobj):
2714 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002715 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717 def SetProperty(name, setting, unset_error_ok=False):
2718 fullname = 'rietveld.' + name
2719 if setting in keyvals:
2720 RunGit(['config', fullname, keyvals[setting]])
2721 else:
2722 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2723
2724 SetProperty('server', 'CODE_REVIEW_SERVER')
2725 # Only server setting is required. Other settings can be absent.
2726 # In that case, we ignore errors raised during option deletion attempt.
2727 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002728 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2730 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002731 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002732 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002733 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2734 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002735 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002736 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002737 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002738 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2739 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002740
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002741 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002742 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002743
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002744 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2745 RunGit(['config', 'gerrit.squash-uploads',
2746 keyvals['GERRIT_SQUASH_UPLOADS']])
2747
tandrii@chromium.org28253532016-04-14 13:46:56 +00002748 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002749 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002750 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2751
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002752 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2753 #should be of the form
2754 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2755 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2756 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2757 keyvals['ORIGIN_URL_CONFIG']])
2758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002759
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002760def urlretrieve(source, destination):
2761 """urllib is broken for SSL connections via a proxy therefore we
2762 can't use urllib.urlretrieve()."""
2763 with open(destination, 'w') as f:
2764 f.write(urllib2.urlopen(source).read())
2765
2766
ukai@chromium.org712d6102013-11-27 00:52:58 +00002767def hasSheBang(fname):
2768 """Checks fname is a #! script."""
2769 with open(fname) as f:
2770 return f.read(2).startswith('#!')
2771
2772
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002773# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2774def DownloadHooks(*args, **kwargs):
2775 pass
2776
2777
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002778def DownloadGerritHook(force):
2779 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002780
2781 Args:
2782 force: True to update hooks. False to install hooks if not present.
2783 """
2784 if not settings.GetIsGerrit():
2785 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002786 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002787 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2788 if not os.access(dst, os.X_OK):
2789 if os.path.exists(dst):
2790 if not force:
2791 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002792 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002793 print(
2794 'WARNING: installing Gerrit commit-msg hook.\n'
2795 ' This behavior of git cl will soon be disabled.\n'
2796 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002797 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002798 if not hasSheBang(dst):
2799 DieWithError('Not a script: %s\n'
2800 'You need to download from\n%s\n'
2801 'into .git/hooks/commit-msg and '
2802 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002803 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2804 except Exception:
2805 if os.path.exists(dst):
2806 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002807 DieWithError('\nFailed to download hooks.\n'
2808 'You need to download from\n%s\n'
2809 'into .git/hooks/commit-msg and '
2810 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002811
2812
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002813
2814def GetRietveldCodereviewSettingsInteractively():
2815 """Prompt the user for settings."""
2816 server = settings.GetDefaultServerUrl(error_ok=True)
2817 prompt = 'Rietveld server (host[:port])'
2818 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2819 newserver = ask_for_data(prompt + ':')
2820 if not server and not newserver:
2821 newserver = DEFAULT_SERVER
2822 if newserver:
2823 newserver = gclient_utils.UpgradeToHttps(newserver)
2824 if newserver != server:
2825 RunGit(['config', 'rietveld.server', newserver])
2826
2827 def SetProperty(initial, caption, name, is_url):
2828 prompt = caption
2829 if initial:
2830 prompt += ' ("x" to clear) [%s]' % initial
2831 new_val = ask_for_data(prompt + ':')
2832 if new_val == 'x':
2833 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2834 elif new_val:
2835 if is_url:
2836 new_val = gclient_utils.UpgradeToHttps(new_val)
2837 if new_val != initial:
2838 RunGit(['config', 'rietveld.' + name, new_val])
2839
2840 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2841 SetProperty(settings.GetDefaultPrivateFlag(),
2842 'Private flag (rietveld only)', 'private', False)
2843 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2844 'tree-status-url', False)
2845 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2846 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2847 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2848 'run-post-upload-hook', False)
2849
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002850@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002851def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002852 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002853
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002854 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002855 'For Gerrit, see http://crbug.com/603116.')
2856 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002857 parser.add_option('--activate-update', action='store_true',
2858 help='activate auto-updating [rietveld] section in '
2859 '.git/config')
2860 parser.add_option('--deactivate-update', action='store_true',
2861 help='deactivate auto-updating [rietveld] section in '
2862 '.git/config')
2863 options, args = parser.parse_args(args)
2864
2865 if options.deactivate_update:
2866 RunGit(['config', 'rietveld.autoupdate', 'false'])
2867 return
2868
2869 if options.activate_update:
2870 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2871 return
2872
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002873 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002874 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002875 return 0
2876
2877 url = args[0]
2878 if not url.endswith('codereview.settings'):
2879 url = os.path.join(url, 'codereview.settings')
2880
2881 # Load code review settings and download hooks (if available).
2882 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2883 return 0
2884
2885
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002886def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002887 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002888 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2889 branch = ShortBranchName(branchref)
2890 _, args = parser.parse_args(args)
2891 if not args:
2892 print("Current base-url:")
2893 return RunGit(['config', 'branch.%s.base-url' % branch],
2894 error_ok=False).strip()
2895 else:
2896 print("Setting base-url to %s" % args[0])
2897 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2898 error_ok=False).strip()
2899
2900
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002901def color_for_status(status):
2902 """Maps a Changelist status to color, for CMDstatus and other tools."""
2903 return {
2904 'unsent': Fore.RED,
2905 'waiting': Fore.BLUE,
2906 'reply': Fore.YELLOW,
2907 'lgtm': Fore.GREEN,
2908 'commit': Fore.MAGENTA,
2909 'closed': Fore.CYAN,
2910 'error': Fore.WHITE,
2911 }.get(status, Fore.WHITE)
2912
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002913def fetch_cl_status(branch, auth_config=None):
2914 """Fetches information for an issue and returns (branch, issue, status)."""
2915 cl = Changelist(branchref=branch, auth_config=auth_config)
2916 url = cl.GetIssueURL()
2917 status = cl.GetStatus()
2918
2919 if url and (not status or status == 'error'):
2920 # The issue probably doesn't exist anymore.
2921 url += ' (broken)'
2922
2923 return (branch, url, status)
2924
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002925def get_cl_statuses(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002926 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002927 """Returns a blocking iterable of (branch, issue, color) for given branches.
2928
2929 If fine_grained is true, this will fetch CL statuses from the server.
2930 Otherwise, simply indicate if there's a matching url for the given branches.
2931
2932 If max_processes is specified, it is used as the maximum number of processes
2933 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2934 spawned.
2935 """
2936 # Silence upload.py otherwise it becomes unwieldly.
2937 upload.verbosity = 0
2938
2939 if fine_grained:
2940 # Process one branch synchronously to work through authentication, then
2941 # spawn processes to process all the other branches in parallel.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002942 if branches:
2943 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2944 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002945
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002946 branches_to_fetch = branches[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002947 pool = ThreadPool(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002948 min(max_processes, len(branches_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002949 if max_processes is not None
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002950 else len(branches_to_fetch))
2951 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002952 yield x
2953 else:
2954 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002955 for b in branches:
2956 cl = Changelist(branchref=b, auth_config=auth_config)
2957 url = cl.GetIssueURL()
2958 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002959
rmistry@google.com2dd99862015-06-22 12:22:18 +00002960
2961def upload_branch_deps(cl, args):
2962 """Uploads CLs of local branches that are dependents of the current branch.
2963
2964 If the local branch dependency tree looks like:
2965 test1 -> test2.1 -> test3.1
2966 -> test3.2
2967 -> test2.2 -> test3.3
2968
2969 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2970 run on the dependent branches in this order:
2971 test2.1, test3.1, test3.2, test2.2, test3.3
2972
2973 Note: This function does not rebase your local dependent branches. Use it when
2974 you make a change to the parent branch that will not conflict with its
2975 dependent branches, and you would like their dependencies updated in
2976 Rietveld.
2977 """
2978 if git_common.is_dirty_git_tree('upload-branch-deps'):
2979 return 1
2980
2981 root_branch = cl.GetBranch()
2982 if root_branch is None:
2983 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2984 'Get on a branch!')
2985 if not cl.GetIssue() or not cl.GetPatchset():
2986 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2987 'patchset dependencies without an uploaded CL.')
2988
2989 branches = RunGit(['for-each-ref',
2990 '--format=%(refname:short) %(upstream:short)',
2991 'refs/heads'])
2992 if not branches:
2993 print('No local branches found.')
2994 return 0
2995
2996 # Create a dictionary of all local branches to the branches that are dependent
2997 # on it.
2998 tracked_to_dependents = collections.defaultdict(list)
2999 for b in branches.splitlines():
3000 tokens = b.split()
3001 if len(tokens) == 2:
3002 branch_name, tracked = tokens
3003 tracked_to_dependents[tracked].append(branch_name)
3004
3005 print
3006 print 'The dependent local branches of %s are:' % root_branch
3007 dependents = []
3008 def traverse_dependents_preorder(branch, padding=''):
3009 dependents_to_process = tracked_to_dependents.get(branch, [])
3010 padding += ' '
3011 for dependent in dependents_to_process:
3012 print '%s%s' % (padding, dependent)
3013 dependents.append(dependent)
3014 traverse_dependents_preorder(dependent, padding)
3015 traverse_dependents_preorder(root_branch)
3016 print
3017
3018 if not dependents:
3019 print 'There are no dependent local branches for %s' % root_branch
3020 return 0
3021
3022 print ('This command will checkout all dependent branches and run '
3023 '"git cl upload".')
3024 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3025
andybons@chromium.org962f9462016-02-03 20:00:42 +00003026 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003027 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003028 args.extend(['-t', 'Updated patchset dependency'])
3029
rmistry@google.com2dd99862015-06-22 12:22:18 +00003030 # Record all dependents that failed to upload.
3031 failures = {}
3032 # Go through all dependents, checkout the branch and upload.
3033 try:
3034 for dependent_branch in dependents:
3035 print
3036 print '--------------------------------------'
3037 print 'Running "git cl upload" from %s:' % dependent_branch
3038 RunGit(['checkout', '-q', dependent_branch])
3039 print
3040 try:
3041 if CMDupload(OptionParser(), args) != 0:
3042 print 'Upload failed for %s!' % dependent_branch
3043 failures[dependent_branch] = 1
3044 except: # pylint: disable=W0702
3045 failures[dependent_branch] = 1
3046 print
3047 finally:
3048 # Swap back to the original root branch.
3049 RunGit(['checkout', '-q', root_branch])
3050
3051 print
3052 print 'Upload complete for dependent branches!'
3053 for dependent_branch in dependents:
3054 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3055 print ' %s : %s' % (dependent_branch, upload_status)
3056 print
3057
3058 return 0
3059
3060
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003061def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003062 """Show status of changelists.
3063
3064 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003065 - Red not sent for review or broken
3066 - Blue waiting for review
3067 - Yellow waiting for you to reply to review
3068 - Green LGTM'ed
3069 - Magenta in the commit queue
3070 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003071
3072 Also see 'git cl comments'.
3073 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003074 parser.add_option('--field',
3075 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003076 parser.add_option('-f', '--fast', action='store_true',
3077 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003078 parser.add_option(
3079 '-j', '--maxjobs', action='store', type=int,
3080 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003081
3082 auth.add_auth_options(parser)
3083 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003084 if args:
3085 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003086 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003088 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003089 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090 if options.field.startswith('desc'):
3091 print cl.GetDescription()
3092 elif options.field == 'id':
3093 issueid = cl.GetIssue()
3094 if issueid:
3095 print issueid
3096 elif options.field == 'patch':
3097 patchset = cl.GetPatchset()
3098 if patchset:
3099 print patchset
3100 elif options.field == 'url':
3101 url = cl.GetIssueURL()
3102 if url:
3103 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003104 return 0
3105
3106 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3107 if not branches:
3108 print('No local branch found.')
3109 return 0
3110
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003111 changes = (
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003112 Changelist(branchref=b, auth_config=auth_config)
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003113 for b in branches.splitlines())
3114 # TODO(tandrii): refactor to use CLs list instead of branches list.
3115 branches = [c.GetBranch() for c in changes]
3116 alignment = max(5, max(len(b) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003117 print 'Branches associated with reviews:'
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003118 output = get_cl_statuses(branches,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003119 fine_grained=not options.fast,
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003120 max_processes=options.maxjobs,
3121 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003122
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003123 branch_statuses = {}
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003124 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
3125 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003126 while branch not in branch_statuses:
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003127 b, i, status = output.next()
3128 branch_statuses[b] = (i, status)
3129 issue_url, status = branch_statuses.pop(branch)
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003130 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003131 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003132 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003133 color = ''
3134 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003135 status_str = '(%s)' % status if status else ''
3136 print ' %*s : %s%s %s%s' % (
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003137 alignment, ShortBranchName(branch), color, issue_url, status_str,
3138 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003139
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003140 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003141 print
3142 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003143 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003144 if not cl.GetIssue():
3145 print 'No issue assigned.'
3146 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003147 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003148 if not options.fast:
3149 print 'Issue description:'
3150 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003151 return 0
3152
3153
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003154def colorize_CMDstatus_doc():
3155 """To be called once in main() to add colors to git cl status help."""
3156 colors = [i for i in dir(Fore) if i[0].isupper()]
3157
3158 def colorize_line(line):
3159 for color in colors:
3160 if color in line.upper():
3161 # Extract whitespaces first and the leading '-'.
3162 indent = len(line) - len(line.lstrip(' ')) + 1
3163 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3164 return line
3165
3166 lines = CMDstatus.__doc__.splitlines()
3167 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3168
3169
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003170@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003171def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003172 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003173
3174 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003175 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003176 parser.add_option('-r', '--reverse', action='store_true',
3177 help='Lookup the branch(es) for the specified issues. If '
3178 'no issues are specified, all branches with mapped '
3179 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003180 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003181 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003182 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003183
dnj@chromium.org406c4402015-03-03 17:22:28 +00003184 if options.reverse:
3185 branches = RunGit(['for-each-ref', 'refs/heads',
3186 '--format=%(refname:short)']).splitlines()
3187
3188 # Reverse issue lookup.
3189 issue_branch_map = {}
3190 for branch in branches:
3191 cl = Changelist(branchref=branch)
3192 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3193 if not args:
3194 args = sorted(issue_branch_map.iterkeys())
3195 for issue in args:
3196 if not issue:
3197 continue
3198 print 'Branch for issue number %s: %s' % (
3199 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3200 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003201 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003202 if len(args) > 0:
3203 try:
3204 issue = int(args[0])
3205 except ValueError:
3206 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003207 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003208 cl.SetIssue(issue)
3209 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003210 return 0
3211
3212
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003213def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003214 """Shows or posts review comments for any changelist."""
3215 parser.add_option('-a', '--add-comment', dest='comment',
3216 help='comment to add to an issue')
3217 parser.add_option('-i', dest='issue',
3218 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003219 parser.add_option('-j', '--json-file',
3220 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003221 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003222 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003223 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003224
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003225 issue = None
3226 if options.issue:
3227 try:
3228 issue = int(options.issue)
3229 except ValueError:
3230 DieWithError('A review issue id is expected to be a number')
3231
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003232 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003233
3234 if options.comment:
3235 cl.AddComment(options.comment)
3236 return 0
3237
3238 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003239 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003240 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003241 summary.append({
3242 'date': message['date'],
3243 'lgtm': False,
3244 'message': message['text'],
3245 'not_lgtm': False,
3246 'sender': message['sender'],
3247 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003248 if message['disapproval']:
3249 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003250 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003251 elif message['approval']:
3252 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003253 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003254 elif message['sender'] == data['owner_email']:
3255 color = Fore.MAGENTA
3256 else:
3257 color = Fore.BLUE
3258 print '\n%s%s %s%s' % (
3259 color, message['date'].split('.', 1)[0], message['sender'],
3260 Fore.RESET)
3261 if message['text'].strip():
3262 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003263 if options.json_file:
3264 with open(options.json_file, 'wb') as f:
3265 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003266 return 0
3267
3268
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003269@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003270def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003271 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003272 parser.add_option('-d', '--display', action='store_true',
3273 help='Display the description instead of opening an editor')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003274
3275 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003276 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003277 options, args = parser.parse_args(args)
3278 _process_codereview_select_options(parser, options)
3279
3280 target_issue = None
3281 if len(args) > 0:
3282 issue_arg = ParseIssueNumberArgument(args[0])
3283 if not issue_arg.valid:
3284 parser.print_help()
3285 return 1
3286 target_issue = issue_arg.issue
3287
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003288 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003289
3290 cl = Changelist(
3291 auth_config=auth_config, issue=target_issue,
3292 codereview=options.forced_codereview)
3293
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003294 if not cl.GetIssue():
3295 DieWithError('This branch has no associated changelist.')
3296 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003297 if options.display:
tandrii@chromium.org8c3b4422016-04-27 13:11:18 +00003298 print description.description
smut@google.com34fb6b12015-07-13 20:03:26 +00003299 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003300 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003301 if cl.GetDescription() != description.description:
3302 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003303 return 0
3304
3305
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003306def CreateDescriptionFromLog(args):
3307 """Pulls out the commit log to use as a base for the CL description."""
3308 log_args = []
3309 if len(args) == 1 and not args[0].endswith('.'):
3310 log_args = [args[0] + '..']
3311 elif len(args) == 1 and args[0].endswith('...'):
3312 log_args = [args[0][:-1]]
3313 elif len(args) == 2:
3314 log_args = [args[0] + '..' + args[1]]
3315 else:
3316 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003317 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003318
3319
thestig@chromium.org44202a22014-03-11 19:22:18 +00003320def CMDlint(parser, args):
3321 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003322 parser.add_option('--filter', action='append', metavar='-x,+y',
3323 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003324 auth.add_auth_options(parser)
3325 options, args = parser.parse_args(args)
3326 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003327
3328 # Access to a protected member _XX of a client class
3329 # pylint: disable=W0212
3330 try:
3331 import cpplint
3332 import cpplint_chromium
3333 except ImportError:
3334 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3335 return 1
3336
3337 # Change the current working directory before calling lint so that it
3338 # shows the correct base.
3339 previous_cwd = os.getcwd()
3340 os.chdir(settings.GetRoot())
3341 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003342 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003343 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3344 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003345 if not files:
3346 print "Cannot lint an empty CL"
3347 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003348
3349 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003350 command = args + files
3351 if options.filter:
3352 command = ['--filter=' + ','.join(options.filter)] + command
3353 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003354
3355 white_regex = re.compile(settings.GetLintRegex())
3356 black_regex = re.compile(settings.GetLintIgnoreRegex())
3357 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3358 for filename in filenames:
3359 if white_regex.match(filename):
3360 if black_regex.match(filename):
3361 print "Ignoring file %s" % filename
3362 else:
3363 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3364 extra_check_functions)
3365 else:
3366 print "Skipping file %s" % filename
3367 finally:
3368 os.chdir(previous_cwd)
3369 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3370 if cpplint._cpplint_state.error_count != 0:
3371 return 1
3372 return 0
3373
3374
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003375def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003376 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003377 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003378 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003379 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003380 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003381 auth.add_auth_options(parser)
3382 options, args = parser.parse_args(args)
3383 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003384
sbc@chromium.org71437c02015-04-09 19:29:40 +00003385 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003386 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003387 return 1
3388
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003389 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003390 if args:
3391 base_branch = args[0]
3392 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003393 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003394 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003395
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003396 cl.RunHook(
3397 committing=not options.upload,
3398 may_prompt=False,
3399 verbose=options.verbose,
3400 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003401 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003402
3403
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003404def GenerateGerritChangeId(message):
3405 """Returns Ixxxxxx...xxx change id.
3406
3407 Works the same way as
3408 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3409 but can be called on demand on all platforms.
3410
3411 The basic idea is to generate git hash of a state of the tree, original commit
3412 message, author/committer info and timestamps.
3413 """
3414 lines = []
3415 tree_hash = RunGitSilent(['write-tree'])
3416 lines.append('tree %s' % tree_hash.strip())
3417 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3418 if code == 0:
3419 lines.append('parent %s' % parent.strip())
3420 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3421 lines.append('author %s' % author.strip())
3422 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3423 lines.append('committer %s' % committer.strip())
3424 lines.append('')
3425 # Note: Gerrit's commit-hook actually cleans message of some lines and
3426 # whitespace. This code is not doing this, but it clearly won't decrease
3427 # entropy.
3428 lines.append(message)
3429 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3430 stdin='\n'.join(lines))
3431 return 'I%s' % change_hash.strip()
3432
3433
wittman@chromium.org455dc922015-01-26 20:15:50 +00003434def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3435 """Computes the remote branch ref to use for the CL.
3436
3437 Args:
3438 remote (str): The git remote for the CL.
3439 remote_branch (str): The git remote branch for the CL.
3440 target_branch (str): The target branch specified by the user.
3441 pending_prefix (str): The pending prefix from the settings.
3442 """
3443 if not (remote and remote_branch):
3444 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003445
wittman@chromium.org455dc922015-01-26 20:15:50 +00003446 if target_branch:
3447 # Cannonicalize branch references to the equivalent local full symbolic
3448 # refs, which are then translated into the remote full symbolic refs
3449 # below.
3450 if '/' not in target_branch:
3451 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3452 else:
3453 prefix_replacements = (
3454 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3455 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3456 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3457 )
3458 match = None
3459 for regex, replacement in prefix_replacements:
3460 match = re.search(regex, target_branch)
3461 if match:
3462 remote_branch = target_branch.replace(match.group(0), replacement)
3463 break
3464 if not match:
3465 # This is a branch path but not one we recognize; use as-is.
3466 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003467 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3468 # Handle the refs that need to land in different refs.
3469 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003470
wittman@chromium.org455dc922015-01-26 20:15:50 +00003471 # Create the true path to the remote branch.
3472 # Does the following translation:
3473 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3474 # * refs/remotes/origin/master -> refs/heads/master
3475 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3476 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3477 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3478 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3479 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3480 'refs/heads/')
3481 elif remote_branch.startswith('refs/remotes/branch-heads'):
3482 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3483 # If a pending prefix exists then replace refs/ with it.
3484 if pending_prefix:
3485 remote_branch = remote_branch.replace('refs/', pending_prefix)
3486 return remote_branch
3487
3488
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003489def cleanup_list(l):
3490 """Fixes a list so that comma separated items are put as individual items.
3491
3492 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3493 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3494 """
3495 items = sum((i.split(',') for i in l), [])
3496 stripped_items = (i.strip() for i in items)
3497 return sorted(filter(None, stripped_items))
3498
3499
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003500@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003501def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003502 """Uploads the current changelist to codereview.
3503
3504 Can skip dependency patchset uploads for a branch by running:
3505 git config branch.branch_name.skip-deps-uploads True
3506 To unset run:
3507 git config --unset branch.branch_name.skip-deps-uploads
3508 Can also set the above globally by using the --global flag.
3509 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003510 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3511 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003512 parser.add_option('--bypass-watchlists', action='store_true',
3513 dest='bypass_watchlists',
3514 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003515 parser.add_option('-f', action='store_true', dest='force',
3516 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003517 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003518 parser.add_option('-t', dest='title',
3519 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003520 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003521 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003522 help='reviewer email addresses')
3523 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003524 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003525 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003526 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003527 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003528 parser.add_option('--emulate_svn_auto_props',
3529 '--emulate-svn-auto-props',
3530 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003531 dest="emulate_svn_auto_props",
3532 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003533 parser.add_option('-c', '--use-commit-queue', action='store_true',
3534 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003535 parser.add_option('--private', action='store_true',
3536 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003537 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003538 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003539 metavar='TARGET',
3540 help='Apply CL to remote ref TARGET. ' +
3541 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003542 parser.add_option('--squash', action='store_true',
3543 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003544 parser.add_option('--no-squash', action='store_true',
3545 help='Don\'t squash multiple commits into one ' +
3546 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003547 parser.add_option('--email', default=None,
3548 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003549 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3550 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003551 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3552 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003553 help='Send the patchset to do a CQ dry run right after '
3554 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003555 parser.add_option('--dependencies', action='store_true',
3556 help='Uploads CLs of all the local branches that depend on '
3557 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003558
rmistry@google.com2dd99862015-06-22 12:22:18 +00003559 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003560 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003561 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003562 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003563 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003564 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003565 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003566
sbc@chromium.org71437c02015-04-09 19:29:40 +00003567 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003568 return 1
3569
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003570 options.reviewers = cleanup_list(options.reviewers)
3571 options.cc = cleanup_list(options.cc)
3572
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003573 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3574 settings.GetIsGerrit()
3575
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003576 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003577 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003578
3579
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003580def IsSubmoduleMergeCommit(ref):
3581 # When submodules are added to the repo, we expect there to be a single
3582 # non-git-svn merge commit at remote HEAD with a signature comment.
3583 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003584 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003585 return RunGit(cmd) != ''
3586
3587
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003589 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003590
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003591 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3592 upstream and closes the issue automatically and atomically.
3593
3594 Otherwise (in case of Rietveld):
3595 Squashes branch into a single commit.
3596 Updates changelog with metadata (e.g. pointer to review).
3597 Pushes/dcommits the code upstream.
3598 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003599 """
3600 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3601 help='bypass upload presubmit hook')
3602 parser.add_option('-m', dest='message',
3603 help="override review description")
3604 parser.add_option('-f', action='store_true', dest='force',
3605 help="force yes to questions (don't prompt)")
3606 parser.add_option('-c', dest='contributor',
3607 help="external contributor for patch (appended to " +
3608 "description and used as author for git). Should be " +
3609 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003610 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003613 auth_config = auth.extract_auth_config_from_options(options)
3614
3615 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003617 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3618 if cl.IsGerrit():
3619 if options.message:
3620 # This could be implemented, but it requires sending a new patch to
3621 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3622 # Besides, Gerrit has the ability to change the commit message on submit
3623 # automatically, thus there is no need to support this option (so far?).
3624 parser.error('-m MESSAGE option is not supported for Gerrit.')
3625 if options.contributor:
3626 parser.error(
3627 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3628 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3629 'the contributor\'s "name <email>". If you can\'t upload such a '
3630 'commit for review, contact your repository admin and request'
3631 '"Forge-Author" permission.')
3632 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3633 options.verbose)
3634
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003635 current = cl.GetBranch()
3636 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3637 if not settings.GetIsGitSvn() and remote == '.':
3638 print
3639 print 'Attempting to push branch %r into another local branch!' % current
3640 print
3641 print 'Either reparent this branch on top of origin/master:'
3642 print ' git reparent-branch --root'
3643 print
3644 print 'OR run `git rebase-update` if you think the parent branch is already'
3645 print 'committed.'
3646 print
3647 print ' Current parent: %r' % upstream_branch
3648 return 1
3649
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003650 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003651 # Default to merging against our best guess of the upstream branch.
3652 args = [cl.GetUpstreamBranch()]
3653
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003654 if options.contributor:
3655 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3656 print "Please provide contibutor as 'First Last <email@example.com>'"
3657 return 1
3658
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003659 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003660 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661
sbc@chromium.org71437c02015-04-09 19:29:40 +00003662 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003663 return 1
3664
3665 # This rev-list syntax means "show all commits not in my branch that
3666 # are in base_branch".
3667 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3668 base_branch]).splitlines()
3669 if upstream_commits:
3670 print ('Base branch "%s" has %d commits '
3671 'not in this branch.' % (base_branch, len(upstream_commits)))
3672 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3673 return 1
3674
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003675 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003676 svn_head = None
3677 if cmd == 'dcommit' or base_has_submodules:
3678 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3679 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003680
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003682 # If the base_head is a submodule merge commit, the first parent of the
3683 # base_head should be a git-svn commit, which is what we're interested in.
3684 base_svn_head = base_branch
3685 if base_has_submodules:
3686 base_svn_head += '^1'
3687
3688 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003689 if extra_commits:
3690 print ('This branch has %d additional commits not upstreamed yet.'
3691 % len(extra_commits.splitlines()))
3692 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3693 'before attempting to %s.' % (base_branch, cmd))
3694 return 1
3695
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003696 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003697 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003698 author = None
3699 if options.contributor:
3700 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003701 hook_results = cl.RunHook(
3702 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003703 may_prompt=not options.force,
3704 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003705 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003706 if not hook_results.should_continue():
3707 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003708
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003709 # Check the tree status if the tree status URL is set.
3710 status = GetTreeStatus()
3711 if 'closed' == status:
3712 print('The tree is closed. Please wait for it to reopen. Use '
3713 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3714 return 1
3715 elif 'unknown' == status:
3716 print('Unable to determine tree status. Please verify manually and '
3717 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3718 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003720 change_desc = ChangeDescription(options.message)
3721 if not change_desc.description and cl.GetIssue():
3722 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003723
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003724 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003725 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003726 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003727 else:
3728 print 'No description set.'
3729 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3730 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003731
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003732 # Keep a separate copy for the commit message, because the commit message
3733 # contains the link to the Rietveld issue, while the Rietveld message contains
3734 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003735 # Keep a separate copy for the commit message.
3736 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003737 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003738
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003739 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003740 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003741 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003742 # after it. Add a period on a new line to circumvent this. Also add a space
3743 # before the period to make sure that Gitiles continues to correctly resolve
3744 # the URL.
3745 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003746 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003747 commit_desc.append_footer('Patch from %s.' % options.contributor)
3748
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003749 print('Description:')
3750 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003751
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003752 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003753 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003754 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003755
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003756 # We want to squash all this branch's commits into one commit with the proper
3757 # description. We do this by doing a "reset --soft" to the base branch (which
3758 # keeps the working copy the same), then dcommitting that. If origin/master
3759 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3760 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003761 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003762 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3763 # Delete the branches if they exist.
3764 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3765 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3766 result = RunGitWithCode(showref_cmd)
3767 if result[0] == 0:
3768 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003769
3770 # We might be in a directory that's present in this branch but not in the
3771 # trunk. Move up to the top of the tree so that git commands that expect a
3772 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003773 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003774 if rel_base_path:
3775 os.chdir(rel_base_path)
3776
3777 # Stuff our change into the merge branch.
3778 # We wrap in a try...finally block so if anything goes wrong,
3779 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003780 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003781 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003782 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003783 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003785 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003786 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003787 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003788 RunGit(
3789 [
3790 'commit', '--author', options.contributor,
3791 '-m', commit_desc.description,
3792 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003793 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003794 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003795 if base_has_submodules:
3796 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3797 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3798 RunGit(['checkout', CHERRY_PICK_BRANCH])
3799 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003800 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003801 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003802 mirror = settings.GetGitMirror(remote)
3803 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003804 pending_prefix = settings.GetPendingRefPrefix()
3805 if not pending_prefix or branch.startswith(pending_prefix):
3806 # If not using refs/pending/heads/* at all, or target ref is already set
3807 # to pending, then push to the target ref directly.
3808 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003809 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003810 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003811 else:
3812 # Cherry-pick the change on top of pending ref and then push it.
3813 assert branch.startswith('refs/'), branch
3814 assert pending_prefix[-1] == '/', pending_prefix
3815 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003816 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003817 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003818 if retcode == 0:
3819 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820 else:
3821 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003822 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003823 'svn', 'dcommit',
3824 '-C%s' % options.similarity,
3825 '--no-rebase', '--rmdir',
3826 ]
3827 if settings.GetForceHttpsCommitUrl():
3828 # Allow forcing https commit URLs for some projects that don't allow
3829 # committing to http URLs (like Google Code).
3830 remote_url = cl.GetGitSvnRemoteUrl()
3831 if urlparse.urlparse(remote_url).scheme == 'http':
3832 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003833 cmd_args.append('--commit-url=%s' % remote_url)
3834 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003835 if 'Committed r' in output:
3836 revision = re.match(
3837 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3838 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003839 finally:
3840 # And then swap back to the original branch and clean up.
3841 RunGit(['checkout', '-q', cl.GetBranch()])
3842 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003843 if base_has_submodules:
3844 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003846 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003847 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003848 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003849
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003850 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003851 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003852 try:
3853 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3854 # We set pushed_to_pending to False, since it made it all the way to the
3855 # real ref.
3856 pushed_to_pending = False
3857 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003858 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003859
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003861 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003862 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003863 if not to_pending:
3864 if viewvc_url and revision:
3865 change_desc.append_footer(
3866 'Committed: %s%s' % (viewvc_url, revision))
3867 elif revision:
3868 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003869 print ('Closing issue '
3870 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003871 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003872 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003873 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003874 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003875 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003876 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003877 if options.bypass_hooks:
3878 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3879 else:
3880 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003881 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003882 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003883
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003884 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003885 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3886 print 'The commit is in the pending queue (%s).' % pending_ref
3887 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003888 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003889 'footer.' % branch)
3890
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003891 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3892 if os.path.isfile(hook):
3893 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003894
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003895 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003896
3897
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003898def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3899 print
3900 print 'Waiting for commit to be landed on %s...' % real_ref
3901 print '(If you are impatient, you may Ctrl-C once without harm)'
3902 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3903 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003904 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003905
3906 loop = 0
3907 while True:
3908 sys.stdout.write('fetching (%d)... \r' % loop)
3909 sys.stdout.flush()
3910 loop += 1
3911
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003912 if mirror:
3913 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003914 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3915 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3916 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3917 for commit in commits.splitlines():
3918 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3919 print 'Found commit on %s' % real_ref
3920 return commit
3921
3922 current_rev = to_rev
3923
3924
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003925def PushToGitPending(remote, pending_ref, upstream_ref):
3926 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3927
3928 Returns:
3929 (retcode of last operation, output log of last operation).
3930 """
3931 assert pending_ref.startswith('refs/'), pending_ref
3932 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3933 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3934 code = 0
3935 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003936 max_attempts = 3
3937 attempts_left = max_attempts
3938 while attempts_left:
3939 if attempts_left != max_attempts:
3940 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3941 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003942
3943 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003944 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003945 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003946 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003947 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003948 print 'Fetch failed with exit code %d.' % code
3949 if out.strip():
3950 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003951 continue
3952
3953 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003954 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003955 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003956 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003957 if code:
3958 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003959 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3960 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003961 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3962 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003963 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003964 return code, out
3965
3966 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003967 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003968 code, out = RunGitWithCode(
3969 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3970 if code == 0:
3971 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003972 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003973 return code, out
3974
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003975 print 'Push failed with exit code %d.' % code
3976 if out.strip():
3977 print out.strip()
3978 if IsFatalPushFailure(out):
3979 print (
3980 'Fatal push error. Make sure your .netrc credentials and git '
3981 'user.email are correct and you have push access to the repo.')
3982 return code, out
3983
3984 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003985 return code, out
3986
3987
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003988def IsFatalPushFailure(push_stdout):
3989 """True if retrying push won't help."""
3990 return '(prohibited by Gerrit)' in push_stdout
3991
3992
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003993@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003995 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003997 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003998 # If it looks like previous commits were mirrored with git-svn.
3999 message = """This repository appears to be a git-svn mirror, but no
4000upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4001 else:
4002 message = """This doesn't appear to be an SVN repository.
4003If your project has a true, writeable git repository, you probably want to run
4004'git cl land' instead.
4005If your project has a git mirror of an upstream SVN master, you probably need
4006to run 'git svn init'.
4007
4008Using the wrong command might cause your commit to appear to succeed, and the
4009review to be closed, without actually landing upstream. If you choose to
4010proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004011 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004012 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004013 return SendUpstream(parser, args, 'dcommit')
4014
4015
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004016@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004017def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004018 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004019 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004020 print('This appears to be an SVN repository.')
4021 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004022 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004023 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004024 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004025
4026
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004027@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004028def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004029 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004030 parser.add_option('-b', dest='newbranch',
4031 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004032 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004033 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004034 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4035 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004036 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004037 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004038 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004039 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004040 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004041 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004042
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004043
4044 group = optparse.OptionGroup(
4045 parser,
4046 'Options for continuing work on the current issue uploaded from a '
4047 'different clone (e.g. different machine). Must be used independently '
4048 'from the other options. No issue number should be specified, and the '
4049 'branch must have an issue number associated with it')
4050 group.add_option('--reapply', action='store_true', dest='reapply',
4051 help='Reset the branch and reapply the issue.\n'
4052 'CAUTION: This will undo any local changes in this '
4053 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004054
4055 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004056 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004057 parser.add_option_group(group)
4058
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004059 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004060 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004061 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004062 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004063 auth_config = auth.extract_auth_config_from_options(options)
4064
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004065 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004066
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004067 issue_arg = None
4068 if options.reapply :
4069 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004070 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004071
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004072 issue_arg = cl.GetIssue()
4073 upstream = cl.GetUpstreamBranch()
4074 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004075 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004076
4077 RunGit(['reset', '--hard', upstream])
4078 if options.pull:
4079 RunGit(['pull'])
4080 else:
4081 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004082 parser.error('Must specify issue number or url')
4083 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004084
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004085 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004086 parser.print_help()
4087 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004088
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004089 if cl.IsGerrit():
4090 if options.reject:
4091 parser.error('--reject is not supported with Gerrit codereview.')
4092 if options.nocommit:
4093 parser.error('--nocommit is not supported with Gerrit codereview.')
4094 if options.directory:
4095 parser.error('--directory is not supported with Gerrit codereview.')
4096
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004097 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004098 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004099 return 1
4100
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004101 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004102 if options.reapply:
4103 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004104 if options.force:
4105 RunGit(['branch', '-D', options.newbranch],
4106 stderr=subprocess2.PIPE, error_ok=True)
4107 RunGit(['checkout', '-b', options.newbranch,
4108 Changelist().GetUpstreamBranch()])
4109
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004110 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4111 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004112
4113
4114def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004115 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004116 # Provide a wrapper for git svn rebase to help avoid accidental
4117 # git svn dcommit.
4118 # It's the only command that doesn't use parser at all since we just defer
4119 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004120
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004121 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004122
4123
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004124def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004125 """Fetches the tree status and returns either 'open', 'closed',
4126 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004127 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004128 if url:
4129 status = urllib2.urlopen(url).read().lower()
4130 if status.find('closed') != -1 or status == '0':
4131 return 'closed'
4132 elif status.find('open') != -1 or status == '1':
4133 return 'open'
4134 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004135 return 'unset'
4136
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004137
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004138def GetTreeStatusReason():
4139 """Fetches the tree status from a json url and returns the message
4140 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004141 url = settings.GetTreeStatusUrl()
4142 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004143 connection = urllib2.urlopen(json_url)
4144 status = json.loads(connection.read())
4145 connection.close()
4146 return status['message']
4147
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004148
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004149def GetBuilderMaster(bot_list):
4150 """For a given builder, fetch the master from AE if available."""
4151 map_url = 'https://builders-map.appspot.com/'
4152 try:
4153 master_map = json.load(urllib2.urlopen(map_url))
4154 except urllib2.URLError as e:
4155 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4156 (map_url, e))
4157 except ValueError as e:
4158 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4159 if not master_map:
4160 return None, 'Failed to build master map.'
4161
4162 result_master = ''
4163 for bot in bot_list:
4164 builder = bot.split(':', 1)[0]
4165 master_list = master_map.get(builder, [])
4166 if not master_list:
4167 return None, ('No matching master for builder %s.' % builder)
4168 elif len(master_list) > 1:
4169 return None, ('The builder name %s exists in multiple masters %s.' %
4170 (builder, master_list))
4171 else:
4172 cur_master = master_list[0]
4173 if not result_master:
4174 result_master = cur_master
4175 elif result_master != cur_master:
4176 return None, 'The builders do not belong to the same master.'
4177 return result_master, None
4178
4179
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004180def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004181 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004182 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004183 status = GetTreeStatus()
4184 if 'unset' == status:
4185 print 'You must configure your tree status URL by running "git cl config".'
4186 return 2
4187
4188 print "The tree is %s" % status
4189 print
4190 print GetTreeStatusReason()
4191 if status != 'open':
4192 return 1
4193 return 0
4194
4195
maruel@chromium.org15192402012-09-06 12:38:29 +00004196def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004197 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004198 group = optparse.OptionGroup(parser, "Try job options")
4199 group.add_option(
4200 "-b", "--bot", action="append",
4201 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4202 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004203 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004204 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004205 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004206 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004207 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004208 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004209 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004210 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004211 "-r", "--revision",
4212 help="Revision to use for the try job; default: the "
4213 "revision will be determined by the try server; see "
4214 "its waterfall for more info")
4215 group.add_option(
4216 "-c", "--clobber", action="store_true", default=False,
4217 help="Force a clobber before building; e.g. don't do an "
4218 "incremental build")
4219 group.add_option(
4220 "--project",
4221 help="Override which project to use. Projects are defined "
4222 "server-side to define what default bot set to use")
4223 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004224 "-p", "--property", dest="properties", action="append", default=[],
4225 help="Specify generic properties in the form -p key1=value1 -p "
4226 "key2=value2 etc (buildbucket only). The value will be treated as "
4227 "json if decodable, or as string otherwise.")
4228 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004229 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004230 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004231 "--use-rietveld", action="store_true", default=False,
4232 help="Use Rietveld to trigger try jobs.")
4233 group.add_option(
4234 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4235 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004236 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004237 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004238 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004239 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004240
machenbach@chromium.org45453142015-09-15 08:45:22 +00004241 if options.use_rietveld and options.properties:
4242 parser.error('Properties can only be specified with buildbucket')
4243
4244 # Make sure that all properties are prop=value pairs.
4245 bad_params = [x for x in options.properties if '=' not in x]
4246 if bad_params:
4247 parser.error('Got properties with missing "=": %s' % bad_params)
4248
maruel@chromium.org15192402012-09-06 12:38:29 +00004249 if args:
4250 parser.error('Unknown arguments: %s' % args)
4251
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004252 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004253 if not cl.GetIssue():
4254 parser.error('Need to upload first')
4255
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004256 if cl.IsGerrit():
4257 parser.error(
4258 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4259 'If your project has Commit Queue, dry run is a workaround:\n'
4260 ' git cl set-commit --dry-run')
4261 # Code below assumes Rietveld issue.
4262 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4263
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004264 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004265 if props.get('closed'):
4266 parser.error('Cannot send tryjobs for a closed CL')
4267
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004268 if props.get('private'):
4269 parser.error('Cannot use trybots with private issue')
4270
maruel@chromium.org15192402012-09-06 12:38:29 +00004271 if not options.name:
4272 options.name = cl.GetBranch()
4273
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004274 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004275 options.master, err_msg = GetBuilderMaster(options.bot)
4276 if err_msg:
4277 parser.error('Tryserver master cannot be found because: %s\n'
4278 'Please manually specify the tryserver master'
4279 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004280
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004281 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004282 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004283 if not options.bot:
4284 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004285
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004286 # Get try masters from PRESUBMIT.py files.
4287 masters = presubmit_support.DoGetTryMasters(
4288 change,
4289 change.LocalPaths(),
4290 settings.GetRoot(),
4291 None,
4292 None,
4293 options.verbose,
4294 sys.stdout)
4295 if masters:
4296 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004297
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004298 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4299 options.bot = presubmit_support.DoGetTrySlaves(
4300 change,
4301 change.LocalPaths(),
4302 settings.GetRoot(),
4303 None,
4304 None,
4305 options.verbose,
4306 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004307
4308 if not options.bot:
4309 # Get try masters from cq.cfg if any.
4310 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4311 # location.
4312 cq_cfg = os.path.join(change.RepositoryRoot(),
4313 'infra', 'config', 'cq.cfg')
4314 if os.path.exists(cq_cfg):
4315 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004316 cq_masters = commit_queue.get_master_builder_map(
4317 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004318 for master, builders in cq_masters.iteritems():
4319 for builder in builders:
4320 # Skip presubmit builders, because these will fail without LGTM.
tandrii@chromium.orgc68f7b52016-04-28 19:42:47 +00004321 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004322 if masters:
4323 return masters
4324
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004325 if not options.bot:
4326 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004327
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004328 builders_and_tests = {}
4329 # TODO(machenbach): The old style command-line options don't support
4330 # multiple try masters yet.
4331 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4332 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4333
4334 for bot in old_style:
4335 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004336 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004337 elif ',' in bot:
4338 parser.error('Specify one bot per --bot flag')
4339 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004340 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004341
4342 for bot, tests in new_style:
4343 builders_and_tests.setdefault(bot, []).extend(tests)
4344
4345 # Return a master map with one master to be backwards compatible. The
4346 # master name defaults to an empty string, which will cause the master
4347 # not to be set on rietveld (deprecated).
4348 return {options.master: builders_and_tests}
4349
4350 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004351
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004352 for builders in masters.itervalues():
4353 if any('triggered' in b for b in builders):
4354 print >> sys.stderr, (
4355 'ERROR You are trying to send a job to a triggered bot. This type of'
4356 ' bot requires an\ninitial job from a parent (usually a builder). '
4357 'Instead send your job to the parent.\n'
4358 'Bot list: %s' % builders)
4359 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004360
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004361 patchset = cl.GetMostRecentPatchset()
4362 if patchset and patchset != cl.GetPatchset():
4363 print(
4364 '\nWARNING Mismatch between local config and server. Did a previous '
4365 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4366 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004367 if options.luci:
4368 trigger_luci_job(cl, masters, options)
4369 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004370 try:
4371 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4372 except BuildbucketResponseException as ex:
4373 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004374 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004375 except Exception as e:
4376 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4377 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4378 e, stacktrace)
4379 return 1
4380 else:
4381 try:
4382 cl.RpcServer().trigger_distributed_try_jobs(
4383 cl.GetIssue(), patchset, options.name, options.clobber,
4384 options.revision, masters)
4385 except urllib2.HTTPError as e:
4386 if e.code == 404:
4387 print('404 from rietveld; '
4388 'did you mean to use "git try" instead of "git cl try"?')
4389 return 1
4390 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004391
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004392 for (master, builders) in sorted(masters.iteritems()):
4393 if master:
4394 print 'Master: %s' % master
4395 length = max(len(builder) for builder in builders)
4396 for builder in sorted(builders):
4397 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004398 return 0
4399
4400
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004401def CMDtry_results(parser, args):
4402 group = optparse.OptionGroup(parser, "Try job results options")
4403 group.add_option(
4404 "-p", "--patchset", type=int, help="patchset number if not current.")
4405 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004406 "--print-master", action='store_true', help="print master name as well.")
4407 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004408 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004409 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004410 group.add_option(
4411 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4412 help="Host of buildbucket. The default host is %default.")
4413 parser.add_option_group(group)
4414 auth.add_auth_options(parser)
4415 options, args = parser.parse_args(args)
4416 if args:
4417 parser.error('Unrecognized args: %s' % ' '.join(args))
4418
4419 auth_config = auth.extract_auth_config_from_options(options)
4420 cl = Changelist(auth_config=auth_config)
4421 if not cl.GetIssue():
4422 parser.error('Need to upload first')
4423
4424 if not options.patchset:
4425 options.patchset = cl.GetMostRecentPatchset()
4426 if options.patchset and options.patchset != cl.GetPatchset():
4427 print(
4428 '\nWARNING Mismatch between local config and server. Did a previous '
4429 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4430 'Continuing using\npatchset %s.\n' % options.patchset)
4431 try:
4432 jobs = fetch_try_jobs(auth_config, cl, options)
4433 except BuildbucketResponseException as ex:
4434 print 'Buildbucket error: %s' % ex
4435 return 1
4436 except Exception as e:
4437 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4438 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4439 e, stacktrace)
4440 return 1
4441 print_tryjobs(options, jobs)
4442 return 0
4443
4444
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004445@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004446def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004447 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004448 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004449 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004450 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004452 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004453 if args:
4454 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004455 branch = cl.GetBranch()
4456 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004457 cl = Changelist()
4458 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004459
4460 # Clear configured merge-base, if there is one.
4461 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004462 else:
4463 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004464 return 0
4465
4466
thestig@chromium.org00858c82013-12-02 23:08:03 +00004467def CMDweb(parser, args):
4468 """Opens the current CL in the web browser."""
4469 _, args = parser.parse_args(args)
4470 if args:
4471 parser.error('Unrecognized args: %s' % ' '.join(args))
4472
4473 issue_url = Changelist().GetIssueURL()
4474 if not issue_url:
4475 print >> sys.stderr, 'ERROR No issue to open'
4476 return 1
4477
4478 webbrowser.open(issue_url)
4479 return 0
4480
4481
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004482def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004483 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004484 parser.add_option('-d', '--dry-run', action='store_true',
4485 help='trigger in dry run mode')
4486 parser.add_option('-c', '--clear', action='store_true',
4487 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004488 auth.add_auth_options(parser)
4489 options, args = parser.parse_args(args)
4490 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004491 if args:
4492 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004493 if options.dry_run and options.clear:
4494 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4495
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004496 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004497 if options.clear:
4498 state = _CQState.CLEAR
4499 elif options.dry_run:
4500 state = _CQState.DRY_RUN
4501 else:
4502 state = _CQState.COMMIT
4503 if not cl.GetIssue():
4504 parser.error('Must upload the issue first')
4505 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004506 return 0
4507
4508
groby@chromium.org411034a2013-02-26 15:12:01 +00004509def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004510 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004511 auth.add_auth_options(parser)
4512 options, args = parser.parse_args(args)
4513 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004514 if args:
4515 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004516 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004517 # Ensure there actually is an issue to close.
4518 cl.GetDescription()
4519 cl.CloseIssue()
4520 return 0
4521
4522
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004523def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004524 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004525 auth.add_auth_options(parser)
4526 options, args = parser.parse_args(args)
4527 auth_config = auth.extract_auth_config_from_options(options)
4528 if args:
4529 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004530
4531 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004532 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004533 # Staged changes would be committed along with the patch from last
4534 # upload, hence counted toward the "last upload" side in the final
4535 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004536 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004537 return 1
4538
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004539 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004540 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004541 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004542 if not issue:
4543 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004544 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004545 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004546
4547 # Create a new branch based on the merge-base
4548 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004549 # Clear cached branch in cl object, to avoid overwriting original CL branch
4550 # properties.
4551 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004552 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004553 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004554 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004555 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004556 return rtn
4557
wychen@chromium.org06928532015-02-03 02:11:29 +00004558 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004559 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004560 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004561 finally:
4562 RunGit(['checkout', '-q', branch])
4563 RunGit(['branch', '-D', TMP_BRANCH])
4564
4565 return 0
4566
4567
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004568def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004569 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004570 parser.add_option(
4571 '--no-color',
4572 action='store_true',
4573 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004574 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004575 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004576 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004577
4578 author = RunGit(['config', 'user.email']).strip() or None
4579
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004580 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004581
4582 if args:
4583 if len(args) > 1:
4584 parser.error('Unknown args')
4585 base_branch = args[0]
4586 else:
4587 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004588 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004589
4590 change = cl.GetChange(base_branch, None)
4591 return owners_finder.OwnersFinder(
4592 [f.LocalPath() for f in
4593 cl.GetChange(base_branch, None).AffectedFiles()],
4594 change.RepositoryRoot(), author,
4595 fopen=file, os_path=os.path, glob=glob.glob,
4596 disable_color=options.no_color).run()
4597
4598
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004599def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004600 """Generates a diff command."""
4601 # Generate diff for the current branch's changes.
4602 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4603 upstream_commit, '--' ]
4604
4605 if args:
4606 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004607 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004608 diff_cmd.append(arg)
4609 else:
4610 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004611
4612 return diff_cmd
4613
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004614def MatchingFileType(file_name, extensions):
4615 """Returns true if the file name ends with one of the given extensions."""
4616 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004617
enne@chromium.org555cfe42014-01-29 18:21:39 +00004618@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004619def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004620 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004621 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004622 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004623 parser.add_option('--full', action='store_true',
4624 help='Reformat the full content of all touched files')
4625 parser.add_option('--dry-run', action='store_true',
4626 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004627 parser.add_option('--python', action='store_true',
4628 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004629 parser.add_option('--diff', action='store_true',
4630 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004631 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004632
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004633 # git diff generates paths against the root of the repository. Change
4634 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004635 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004636 if rel_base_path:
4637 os.chdir(rel_base_path)
4638
digit@chromium.org29e47272013-05-17 17:01:46 +00004639 # Grab the merge-base commit, i.e. the upstream commit of the current
4640 # branch when it was created or the last time it was rebased. This is
4641 # to cover the case where the user may have called "git fetch origin",
4642 # moving the origin branch to a newer commit, but hasn't rebased yet.
4643 upstream_commit = None
4644 cl = Changelist()
4645 upstream_branch = cl.GetUpstreamBranch()
4646 if upstream_branch:
4647 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4648 upstream_commit = upstream_commit.strip()
4649
4650 if not upstream_commit:
4651 DieWithError('Could not find base commit for this branch. '
4652 'Are you in detached state?')
4653
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004654 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4655 diff_output = RunGit(changed_files_cmd)
4656 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004657 # Filter out files deleted by this CL
4658 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004659
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004660 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4661 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4662 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004663 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004664
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004665 top_dir = os.path.normpath(
4666 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4667
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004668 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4669 # formatted. This is used to block during the presubmit.
4670 return_value = 0
4671
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004672 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004673 # Locate the clang-format binary in the checkout
4674 try:
4675 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4676 except clang_format.NotFoundError, e:
4677 DieWithError(e)
4678
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004679 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004680 cmd = [clang_format_tool]
4681 if not opts.dry_run and not opts.diff:
4682 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004683 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004684 if opts.diff:
4685 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004686 else:
4687 env = os.environ.copy()
4688 env['PATH'] = str(os.path.dirname(clang_format_tool))
4689 try:
4690 script = clang_format.FindClangFormatScriptInChromiumTree(
4691 'clang-format-diff.py')
4692 except clang_format.NotFoundError, e:
4693 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004694
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004695 cmd = [sys.executable, script, '-p0']
4696 if not opts.dry_run and not opts.diff:
4697 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004698
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004699 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4700 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004701
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004702 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4703 if opts.diff:
4704 sys.stdout.write(stdout)
4705 if opts.dry_run and len(stdout) > 0:
4706 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004707
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004708 # Similar code to above, but using yapf on .py files rather than clang-format
4709 # on C/C++ files
4710 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004711 yapf_tool = gclient_utils.FindExecutable('yapf')
4712 if yapf_tool is None:
4713 DieWithError('yapf not found in PATH')
4714
4715 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004716 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004717 cmd = [yapf_tool]
4718 if not opts.dry_run and not opts.diff:
4719 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004720 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004721 if opts.diff:
4722 sys.stdout.write(stdout)
4723 else:
4724 # TODO(sbc): yapf --lines mode still has some issues.
4725 # https://github.com/google/yapf/issues/154
4726 DieWithError('--python currently only works with --full')
4727
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004728 # Dart's formatter does not have the nice property of only operating on
4729 # modified chunks, so hard code full.
4730 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004731 try:
4732 command = [dart_format.FindDartFmtToolInChromiumTree()]
4733 if not opts.dry_run and not opts.diff:
4734 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004735 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004736
ppi@chromium.org6593d932016-03-03 15:41:15 +00004737 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004738 if opts.dry_run and stdout:
4739 return_value = 2
4740 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004741 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4742 'found in this checkout. Files in other languages are still ' +
4743 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004744
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004745 # Format GN build files. Always run on full build files for canonical form.
4746 if gn_diff_files:
4747 cmd = ['gn', 'format']
4748 if not opts.dry_run and not opts.diff:
4749 cmd.append('--in-place')
4750 for gn_diff_file in gn_diff_files:
4751 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4752 if opts.diff:
4753 sys.stdout.write(stdout)
4754
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004755 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004756
4757
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004758@subcommand.usage('<codereview url or issue id>')
4759def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004760 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004761 _, args = parser.parse_args(args)
4762
4763 if len(args) != 1:
4764 parser.print_help()
4765 return 1
4766
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004767 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004768 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004769 parser.print_help()
4770 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004771 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004772
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004773 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004774 output = RunGit(['config', '--local', '--get-regexp',
4775 r'branch\..*\.%s' % issueprefix],
4776 error_ok=True)
4777 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004778 if issue == target_issue:
4779 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004780
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004781 branches = []
4782 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004783 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004784 if len(branches) == 0:
4785 print 'No branch found for issue %s.' % target_issue
4786 return 1
4787 if len(branches) == 1:
4788 RunGit(['checkout', branches[0]])
4789 else:
4790 print 'Multiple branches match issue %s:' % target_issue
4791 for i in range(len(branches)):
4792 print '%d: %s' % (i, branches[i])
4793 which = raw_input('Choose by index: ')
4794 try:
4795 RunGit(['checkout', branches[int(which)]])
4796 except (IndexError, ValueError):
4797 print 'Invalid selection, not checking out any branch.'
4798 return 1
4799
4800 return 0
4801
4802
maruel@chromium.org29404b52014-09-08 22:58:00 +00004803def CMDlol(parser, args):
4804 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004805 print zlib.decompress(base64.b64decode(
4806 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4807 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4808 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4809 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004810 return 0
4811
4812
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004813class OptionParser(optparse.OptionParser):
4814 """Creates the option parse and add --verbose support."""
4815 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004816 optparse.OptionParser.__init__(
4817 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004818 self.add_option(
4819 '-v', '--verbose', action='count', default=0,
4820 help='Use 2 times for more debugging info')
4821
4822 def parse_args(self, args=None, values=None):
4823 options, args = optparse.OptionParser.parse_args(self, args, values)
4824 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4825 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4826 return options, args
4827
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004829def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004830 if sys.hexversion < 0x02060000:
4831 print >> sys.stderr, (
4832 '\nYour python version %s is unsupported, please upgrade.\n' %
4833 sys.version.split(' ', 1)[0])
4834 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004835
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004836 # Reload settings.
4837 global settings
4838 settings = Settings()
4839
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004840 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004841 dispatcher = subcommand.CommandDispatcher(__name__)
4842 try:
4843 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004844 except auth.AuthenticationError as e:
4845 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004846 except urllib2.HTTPError, e:
4847 if e.code != 500:
4848 raise
4849 DieWithError(
4850 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4851 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004852 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004853
4854
4855if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004856 # These affect sys.stdout so do it outside of main() to simplify mocks in
4857 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004858 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004859 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004860 try:
4861 sys.exit(main(sys.argv[1:]))
4862 except KeyboardInterrupt:
4863 sys.stderr.write('interrupted\n')
4864 sys.exit(1)