blob: 47c646c20a843c0ec853e8ad9ff46f8b710e97ca [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
maruel@chromium.org1033efd2013-07-23 23:25:09 +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
bauerb@chromium.org27386dd2015-02-16 10:45:39 +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.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000838class _ParsedIssueNumberArgument(object):
839 def __init__(self, issue=None, patchset=None, hostname=None):
840 self.issue = issue
841 self.patchset = patchset
842 self.hostname = hostname
843
844 @property
845 def valid(self):
846 return self.issue is not None
847
848
849class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
850 def __init__(self, *args, **kwargs):
851 self.patch_url = kwargs.pop('patch_url', None)
852 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
853
854
855def ParseIssueNumberArgument(arg):
856 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
857 fail_result = _ParsedIssueNumberArgument()
858
859 if arg.isdigit():
860 return _ParsedIssueNumberArgument(issue=int(arg))
861 if not arg.startswith('http'):
862 return fail_result
863 url = gclient_utils.UpgradeToHttps(arg)
864 try:
865 parsed_url = urlparse.urlparse(url)
866 except ValueError:
867 return fail_result
868 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
869 tmp = cls.ParseIssueURL(parsed_url)
870 if tmp is not None:
871 return tmp
872 return fail_result
873
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000876 """Changelist works with one changelist in local branch.
877
878 Supports two codereview backends: Rietveld or Gerrit, selected at object
879 creation.
880
881 Not safe for concurrent multi-{thread,process} use.
882 """
883
884 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
885 """Create a new ChangeList instance.
886
887 If issue is given, the codereview must be given too.
888
889 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
890 Otherwise, it's decided based on current configuration of the local branch,
891 with default being 'rietveld' for backwards compatibility.
892 See _load_codereview_impl for more details.
893
894 **kwargs will be passed directly to codereview implementation.
895 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000897 global settings
898 if not settings:
899 # Happens when git_cl.py is used as a utility library.
900 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901
902 if issue:
903 assert codereview, 'codereview must be known, if issue is known'
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.branchref = branchref
906 if self.branchref:
907 self.branch = ShortBranchName(self.branchref)
908 else:
909 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000911 self.lookedup_issue = False
912 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.has_description = False
914 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000915 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000917 self.cc = None
918 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000922 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000924 assert self._codereview_impl
925 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 def _load_codereview_impl(self, codereview=None, **kwargs):
928 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000929 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
930 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
931 self._codereview = codereview
932 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 return
934
935 # Automatic selection based on issue number set for a current branch.
936 # Rietveld takes precedence over Gerrit.
937 assert not self.issue
938 # Whether we find issue or not, we are doing the lookup.
939 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000940 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 setting = cls.IssueSetting(self.GetBranch())
942 issue = RunGit(['config', setting], error_ok=True).strip()
943 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = cls(self, **kwargs)
946 self.issue = int(issue)
947 return
948
949 # No issue is set for this branch, so decide based on repo-wide settings.
950 return self._load_codereview_impl(
951 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
952 **kwargs)
953
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 def IsGerrit(self):
955 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000956
957 def GetCCList(self):
958 """Return the users cc'd on this CL.
959
960 Return is a string suitable for passing to gcl with the --cc flag.
961 """
962 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000963 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000964 more_cc = ','.join(self.watchers)
965 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
966 return self.cc
967
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000968 def GetCCListWithoutDefault(self):
969 """Return the users cc'd on this CL excluding default ones."""
970 if self.cc is None:
971 self.cc = ','.join(self.watchers)
972 return self.cc
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def SetWatchers(self, watchers):
975 """Set the list of email addresses that should be cc'd based on the changed
976 files in this CL.
977 """
978 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
980 def GetBranch(self):
981 """Returns the short branch name, e.g. 'master'."""
982 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000984 if not branchref:
985 return None
986 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 self.branch = ShortBranchName(self.branchref)
988 return self.branch
989
990 def GetBranchRef(self):
991 """Returns the full branch name, e.g. 'refs/heads/master'."""
992 self.GetBranch() # Poke the lazy loader.
993 return self.branchref
994
tandrii@chromium.org534f67a2016-04-07 18:47:05 +0000995 def ClearBranch(self):
996 """Clears cached branch data of this object."""
997 self.branch = self.branchref = None
998
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000999 @staticmethod
1000 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001001 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 e.g. 'origin', 'refs/heads/master'
1003 """
1004 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1006 error_ok=True).strip()
1007 if upstream_branch:
1008 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1009 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001010 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1011 error_ok=True).strip()
1012 if upstream_branch:
1013 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Fall back on trying a git-svn upstream branch.
1016 if settings.GetIsGitSvn():
1017 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001019 # Else, try to guess the origin remote.
1020 remote_branches = RunGit(['branch', '-r']).split()
1021 if 'origin/master' in remote_branches:
1022 # Fall back on origin/master if it exits.
1023 remote = 'origin'
1024 upstream_branch = 'refs/heads/master'
1025 elif 'origin/trunk' in remote_branches:
1026 # Fall back on origin/trunk if it exists. Generally a shared
1027 # git-svn clone
1028 remote = 'origin'
1029 upstream_branch = 'refs/heads/trunk'
1030 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001031 DieWithError(
1032 'Unable to determine default branch to diff against.\n'
1033 'Either pass complete "git diff"-style arguments, like\n'
1034 ' git cl upload origin/master\n'
1035 'or verify this branch is set up to track another \n'
1036 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
1038 return remote, upstream_branch
1039
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001040 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001041 upstream_branch = self.GetUpstreamBranch()
1042 if not BranchExists(upstream_branch):
1043 DieWithError('The upstream for the current branch (%s) does not exist '
1044 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001045 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001046 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 def GetUpstreamBranch(self):
1049 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001052 upstream_branch = upstream_branch.replace('refs/heads/',
1053 'refs/remotes/%s/' % remote)
1054 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1055 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 self.upstream_branch = upstream_branch
1057 return self.upstream_branch
1058
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001059 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001060 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 remote, branch = None, self.GetBranch()
1062 seen_branches = set()
1063 while branch not in seen_branches:
1064 seen_branches.add(branch)
1065 remote, branch = self.FetchUpstreamTuple(branch)
1066 branch = ShortBranchName(branch)
1067 if remote != '.' or branch.startswith('refs/remotes'):
1068 break
1069 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001070 remotes = RunGit(['remote'], error_ok=True).split()
1071 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001072 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001073 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001075 logging.warning('Could not determine which remote this change is '
1076 'associated with, so defaulting to "%s". This may '
1077 'not be what you want. You may prevent this message '
1078 'by running "git svn info" as documented here: %s',
1079 self._remote,
1080 GIT_INSTRUCTIONS_URL)
1081 else:
1082 logging.warn('Could not determine which remote this change is '
1083 'associated with. You may prevent this message by '
1084 'running "git svn info" as documented here: %s',
1085 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 branch = 'HEAD'
1087 if branch.startswith('refs/remotes'):
1088 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001089 elif branch.startswith('refs/branch-heads/'):
1090 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 else:
1092 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001093 return self._remote
1094
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001095 def GitSanityChecks(self, upstream_git_obj):
1096 """Checks git repo status and ensures diff is from local commits."""
1097
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 if upstream_git_obj is None:
1099 if self.GetBranch() is None:
1100 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001101 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001102 else:
1103 print >> sys.stderr, (
1104 'ERROR: no upstream branch')
1105 return False
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 # Verify the commit we're diffing against is in our current branch.
1108 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1109 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1110 if upstream_sha != common_ancestor:
1111 print >> sys.stderr, (
1112 'ERROR: %s is not in the current branch. You may need to rebase '
1113 'your tracking branch' % upstream_sha)
1114 return False
1115
1116 # List the commits inside the diff, and verify they are all local.
1117 commits_in_diff = RunGit(
1118 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1119 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1120 remote_branch = remote_branch.strip()
1121 if code != 0:
1122 _, remote_branch = self.GetRemoteBranch()
1123
1124 commits_in_remote = RunGit(
1125 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1126
1127 common_commits = set(commits_in_diff) & set(commits_in_remote)
1128 if common_commits:
1129 print >> sys.stderr, (
1130 'ERROR: Your diff contains %d commits already in %s.\n'
1131 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1132 'the diff. If you are using a custom git flow, you can override'
1133 ' the reference used for this check with "git config '
1134 'gitcl.remotebranch <git-ref>".' % (
1135 len(common_commits), remote_branch, upstream_git_obj))
1136 return False
1137 return True
1138
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001139 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001141
1142 Returns None if it is not set.
1143 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001144 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1145 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001146
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001147 def GetGitSvnRemoteUrl(self):
1148 """Return the configured git-svn remote URL parsed from git svn info.
1149
1150 Returns None if it is not set.
1151 """
1152 # URL is dependent on the current directory.
1153 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1154 if data:
1155 keys = dict(line.split(': ', 1) for line in data.splitlines()
1156 if ': ' in line)
1157 return keys.get('URL', None)
1158 return None
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 def GetRemoteUrl(self):
1161 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1162
1163 Returns None if there is no remote.
1164 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001165 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001166 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1167
1168 # If URL is pointing to a local directory, it is probably a git cache.
1169 if os.path.isdir(url):
1170 url = RunGit(['config', 'remote.%s.url' % remote],
1171 error_ok=True,
1172 cwd=url).strip()
1173 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001175 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001176 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001177 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 issue = RunGit(['config',
1179 self._codereview_impl.IssueSetting(self.GetBranch())],
1180 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001181 self.issue = int(issue) or None if issue else None
1182 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 return self.issue
1184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 def GetIssueURL(self):
1186 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001187 issue = self.GetIssue()
1188 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001189 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
1192 def GetDescription(self, pretty=False):
1193 if not self.has_description:
1194 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 self.has_description = True
1197 if pretty:
1198 wrapper = textwrap.TextWrapper()
1199 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1200 return wrapper.fill(self.description)
1201 return self.description
1202
1203 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001204 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 self.patchset = int(patchset) or None if patchset else None
1209 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return self.patchset
1211
1212 def SetPatchset(self, patchset):
1213 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001216 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001220 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001221 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001223 def SetIssue(self, issue=None):
1224 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1226 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001228 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 RunGit(['config', issue_setting, str(issue)])
1230 codereview_server = self._codereview_impl.GetCodereviewServer()
1231 if codereview_server:
1232 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001234 current_issue = self.GetIssue()
1235 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001237 self.issue = None
1238 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001240 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001241 if not self.GitSanityChecks(upstream_branch):
1242 DieWithError('\nGit sanity check failure')
1243
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001244 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001245 if not root:
1246 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001248
1249 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001250 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001251 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001252 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001253 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001254 except subprocess2.CalledProcessError:
1255 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001256 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001257 'This branch probably doesn\'t exist anymore. To reset the\n'
1258 'tracking branch, please run\n'
1259 ' git branch --set-upstream %s trunk\n'
1260 'replacing trunk with origin/master or the relevant branch') %
1261 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001262
maruel@chromium.org52424302012-08-29 15:14:30 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001265 if issue:
1266 description = self.GetDescription()
1267 else:
1268 # If the change was never uploaded, use the log messages of all commits
1269 # up to the branch point, as git cl upload will prefill the description
1270 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001271 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1272 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001273
1274 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001275 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001276 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001277 name,
1278 description,
1279 absroot,
1280 files,
1281 issue,
1282 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001283 author,
1284 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001285
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self._codereview_impl.UpdateDescriptionRemote(description)
1289
1290 def RunHook(self, committing, may_prompt, verbose, change):
1291 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1292 try:
1293 return presubmit_support.DoPresubmitChecks(change, committing,
1294 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1295 default_presubmit=None, may_prompt=may_prompt,
1296 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1297 except presubmit_support.PresubmitFailure, e:
1298 DieWithError(
1299 ('%s\nMaybe your depot_tools is out of date?\n'
1300 'If all fails, contact maruel@') % e)
1301
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001302 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1303 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001304 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1305 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001306 else:
1307 # Assume url.
1308 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1309 urlparse.urlparse(issue_arg))
1310 if not parsed_issue_arg or not parsed_issue_arg.valid:
1311 DieWithError('Failed to parse issue argument "%s". '
1312 'Must be an issue number or a valid URL.' % issue_arg)
1313 return self._codereview_impl.CMDPatchWithParsedIssue(
1314 parsed_issue_arg, reject, nocommit, directory)
1315
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001316 # Forward methods to codereview specific implementation.
1317
1318 def CloseIssue(self):
1319 return self._codereview_impl.CloseIssue()
1320
1321 def GetStatus(self):
1322 return self._codereview_impl.GetStatus()
1323
1324 def GetCodereviewServer(self):
1325 return self._codereview_impl.GetCodereviewServer()
1326
1327 def GetApprovingReviewers(self):
1328 return self._codereview_impl.GetApprovingReviewers()
1329
1330 def GetMostRecentPatchset(self):
1331 return self._codereview_impl.GetMostRecentPatchset()
1332
1333 def __getattr__(self, attr):
1334 # This is because lots of untested code accesses Rietveld-specific stuff
1335 # directly, and it's hard to fix for sure. So, just let it work, and fix
1336 # on a cases by case basis.
1337 return getattr(self._codereview_impl, attr)
1338
1339
1340class _ChangelistCodereviewBase(object):
1341 """Abstract base class encapsulating codereview specifics of a changelist."""
1342 def __init__(self, changelist):
1343 self._changelist = changelist # instance of Changelist
1344
1345 def __getattr__(self, attr):
1346 # Forward methods to changelist.
1347 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1348 # _RietveldChangelistImpl to avoid this hack?
1349 return getattr(self._changelist, attr)
1350
1351 def GetStatus(self):
1352 """Apply a rough heuristic to give a simple summary of an issue's review
1353 or CQ status, assuming adherence to a common workflow.
1354
1355 Returns None if no issue for this branch, or specific string keywords.
1356 """
1357 raise NotImplementedError()
1358
1359 def GetCodereviewServer(self):
1360 """Returns server URL without end slash, like "https://codereview.com"."""
1361 raise NotImplementedError()
1362
1363 def FetchDescription(self):
1364 """Fetches and returns description from the codereview server."""
1365 raise NotImplementedError()
1366
1367 def GetCodereviewServerSetting(self):
1368 """Returns git config setting for the codereview server."""
1369 raise NotImplementedError()
1370
1371 @staticmethod
1372 def IssueSetting(branch):
1373 """Returns name of git config setting which stores issue number for a given
1374 branch."""
1375 raise NotImplementedError()
1376
1377 def PatchsetSetting(self):
1378 """Returns name of git config setting which stores issue number."""
1379 raise NotImplementedError()
1380
1381 def GetRieveldObjForPresubmit(self):
1382 # This is an unfortunate Rietveld-embeddedness in presubmit.
1383 # For non-Rietveld codereviews, this probably should return a dummy object.
1384 raise NotImplementedError()
1385
1386 def UpdateDescriptionRemote(self, description):
1387 """Update the description on codereview site."""
1388 raise NotImplementedError()
1389
1390 def CloseIssue(self):
1391 """Closes the issue."""
1392 raise NotImplementedError()
1393
1394 def GetApprovingReviewers(self):
1395 """Returns a list of reviewers approving the change.
1396
1397 Note: not necessarily committers.
1398 """
1399 raise NotImplementedError()
1400
1401 def GetMostRecentPatchset(self):
1402 """Returns the most recent patchset number from the codereview site."""
1403 raise NotImplementedError()
1404
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001405 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1406 directory):
1407 """Fetches and applies the issue.
1408
1409 Arguments:
1410 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1411 reject: if True, reject the failed patch instead of switching to 3-way
1412 merge. Rietveld only.
1413 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1414 only.
1415 directory: switch to directory before applying the patch. Rietveld only.
1416 """
1417 raise NotImplementedError()
1418
1419 @staticmethod
1420 def ParseIssueURL(parsed_url):
1421 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1422 failed."""
1423 raise NotImplementedError()
1424
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001425
1426class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1427 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1428 super(_RietveldChangelistImpl, self).__init__(changelist)
1429 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1430 settings.GetDefaultServerUrl()
1431
1432 self._rietveld_server = rietveld_server
1433 self._auth_config = auth_config
1434 self._props = None
1435 self._rpc_server = None
1436
1437 def GetAuthConfig(self):
1438 return self._auth_config
1439
1440 def GetCodereviewServer(self):
1441 if not self._rietveld_server:
1442 # If we're on a branch then get the server potentially associated
1443 # with that branch.
1444 if self.GetIssue():
1445 rietveld_server_setting = self.GetCodereviewServerSetting()
1446 if rietveld_server_setting:
1447 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1448 ['config', rietveld_server_setting], error_ok=True).strip())
1449 if not self._rietveld_server:
1450 self._rietveld_server = settings.GetDefaultServerUrl()
1451 return self._rietveld_server
1452
1453 def FetchDescription(self):
1454 issue = self.GetIssue()
1455 assert issue
1456 try:
1457 return self.RpcServer().get_description(issue).strip()
1458 except urllib2.HTTPError as e:
1459 if e.code == 404:
1460 DieWithError(
1461 ('\nWhile fetching the description for issue %d, received a '
1462 '404 (not found)\n'
1463 'error. It is likely that you deleted this '
1464 'issue on the server. If this is the\n'
1465 'case, please run\n\n'
1466 ' git cl issue 0\n\n'
1467 'to clear the association with the deleted issue. Then run '
1468 'this command again.') % issue)
1469 else:
1470 DieWithError(
1471 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1472 except urllib2.URLError as e:
1473 print >> sys.stderr, (
1474 'Warning: Failed to retrieve CL description due to network '
1475 'failure.')
1476 return ''
1477
1478 def GetMostRecentPatchset(self):
1479 return self.GetIssueProperties()['patchsets'][-1]
1480
1481 def GetPatchSetDiff(self, issue, patchset):
1482 return self.RpcServer().get(
1483 '/download/issue%s_%s.diff' % (issue, patchset))
1484
1485 def GetIssueProperties(self):
1486 if self._props is None:
1487 issue = self.GetIssue()
1488 if not issue:
1489 self._props = {}
1490 else:
1491 self._props = self.RpcServer().get_issue_properties(issue, True)
1492 return self._props
1493
1494 def GetApprovingReviewers(self):
1495 return get_approving_reviewers(self.GetIssueProperties())
1496
1497 def AddComment(self, message):
1498 return self.RpcServer().add_comment(self.GetIssue(), message)
1499
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001500 def GetStatus(self):
1501 """Apply a rough heuristic to give a simple summary of an issue's review
1502 or CQ status, assuming adherence to a common workflow.
1503
1504 Returns None if no issue for this branch, or one of the following keywords:
1505 * 'error' - error from review tool (including deleted issues)
1506 * 'unsent' - not sent for review
1507 * 'waiting' - waiting for review
1508 * 'reply' - waiting for owner to reply to review
1509 * 'lgtm' - LGTM from at least one approved reviewer
1510 * 'commit' - in the commit queue
1511 * 'closed' - closed
1512 """
1513 if not self.GetIssue():
1514 return None
1515
1516 try:
1517 props = self.GetIssueProperties()
1518 except urllib2.HTTPError:
1519 return 'error'
1520
1521 if props.get('closed'):
1522 # Issue is closed.
1523 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001524 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001525 # Issue is in the commit queue.
1526 return 'commit'
1527
1528 try:
1529 reviewers = self.GetApprovingReviewers()
1530 except urllib2.HTTPError:
1531 return 'error'
1532
1533 if reviewers:
1534 # Was LGTM'ed.
1535 return 'lgtm'
1536
1537 messages = props.get('messages') or []
1538
1539 if not messages:
1540 # No message was sent.
1541 return 'unsent'
1542 if messages[-1]['sender'] != props.get('owner_email'):
1543 # Non-LGTM reply from non-owner
1544 return 'reply'
1545 return 'waiting'
1546
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001547 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001548 return self.RpcServer().update_description(
1549 self.GetIssue(), self.description)
1550
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001552 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001553
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001554 def SetFlag(self, flag, value):
1555 """Patchset must match."""
1556 if not self.GetPatchset():
1557 DieWithError('The patchset needs to match. Send another patchset.')
1558 try:
1559 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001560 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001561 except urllib2.HTTPError, e:
1562 if e.code == 404:
1563 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1564 if e.code == 403:
1565 DieWithError(
1566 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1567 'match?') % (self.GetIssue(), self.GetPatchset()))
1568 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001569
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001570 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571 """Returns an upload.RpcServer() to access this review's rietveld instance.
1572 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001573 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001574 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001576 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001577 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579 @staticmethod
1580 def IssueSetting(branch):
1581 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584 """Return the git setting that stores this change's most recent patchset."""
1585 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1586
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001587 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001589 branch = self.GetBranch()
1590 if branch:
1591 return 'branch.%s.rietveldserver' % branch
1592 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001594 def GetRieveldObjForPresubmit(self):
1595 return self.RpcServer()
1596
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001597 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1598 directory):
1599 # TODO(maruel): Use apply_issue.py
1600
1601 # PatchIssue should never be called with a dirty tree. It is up to the
1602 # caller to check this, but just in case we assert here since the
1603 # consequences of the caller not checking this could be dire.
1604 assert(not git_common.is_dirty_git_tree('apply'))
1605 assert(parsed_issue_arg.valid)
1606 self._changelist.issue = parsed_issue_arg.issue
1607 if parsed_issue_arg.hostname:
1608 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1609
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001610 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1611 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001612 assert parsed_issue_arg.patchset
1613 patchset = parsed_issue_arg.patchset
1614 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1615 else:
1616 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1617 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1618
1619 # Switch up to the top-level directory, if necessary, in preparation for
1620 # applying the patch.
1621 top = settings.GetRelativeRoot()
1622 if top:
1623 os.chdir(top)
1624
1625 # Git patches have a/ at the beginning of source paths. We strip that out
1626 # with a sed script rather than the -p flag to patch so we can feed either
1627 # Git or svn-style patches into the same apply command.
1628 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1629 try:
1630 patch_data = subprocess2.check_output(
1631 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1632 except subprocess2.CalledProcessError:
1633 DieWithError('Git patch mungling failed.')
1634 logging.info(patch_data)
1635
1636 # We use "git apply" to apply the patch instead of "patch" so that we can
1637 # pick up file adds.
1638 # The --index flag means: also insert into the index (so we catch adds).
1639 cmd = ['git', 'apply', '--index', '-p0']
1640 if directory:
1641 cmd.extend(('--directory', directory))
1642 if reject:
1643 cmd.append('--reject')
1644 elif IsGitVersionAtLeast('1.7.12'):
1645 cmd.append('--3way')
1646 try:
1647 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1648 stdin=patch_data, stdout=subprocess2.VOID)
1649 except subprocess2.CalledProcessError:
1650 print 'Failed to apply the patch'
1651 return 1
1652
1653 # If we had an issue, commit the current state and register the issue.
1654 if not nocommit:
1655 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1656 'patch from issue %(i)s at patchset '
1657 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1658 % {'i': self.GetIssue(), 'p': patchset})])
1659 self.SetIssue(self.GetIssue())
1660 self.SetPatchset(patchset)
1661 print "Committed patch locally."
1662 else:
1663 print "Patch applied to index."
1664 return 0
1665
1666 @staticmethod
1667 def ParseIssueURL(parsed_url):
1668 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1669 return None
1670 # Typical url: https://domain/<issue_number>[/[other]]
1671 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1672 if match:
1673 return _RietveldParsedIssueNumberArgument(
1674 issue=int(match.group(1)),
1675 hostname=parsed_url.netloc)
1676 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1677 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1678 if match:
1679 return _RietveldParsedIssueNumberArgument(
1680 issue=int(match.group(1)),
1681 patchset=int(match.group(2)),
1682 hostname=parsed_url.netloc,
1683 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1684 return None
1685
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001686
1687class _GerritChangelistImpl(_ChangelistCodereviewBase):
1688 def __init__(self, changelist, auth_config=None):
1689 # auth_config is Rietveld thing, kept here to preserve interface only.
1690 super(_GerritChangelistImpl, self).__init__(changelist)
1691 self._change_id = None
1692 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1693 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1694
1695 def _GetGerritHost(self):
1696 # Lazy load of configs.
1697 self.GetCodereviewServer()
1698 return self._gerrit_host
1699
1700 def GetCodereviewServer(self):
1701 if not self._gerrit_server:
1702 # If we're on a branch then get the server potentially associated
1703 # with that branch.
1704 if self.GetIssue():
1705 gerrit_server_setting = self.GetCodereviewServerSetting()
1706 if gerrit_server_setting:
1707 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1708 error_ok=True).strip()
1709 if self._gerrit_server:
1710 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1711 if not self._gerrit_server:
1712 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1713 # has "-review" suffix for lowest level subdomain.
1714 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1715 parts[0] = parts[0] + '-review'
1716 self._gerrit_host = '.'.join(parts)
1717 self._gerrit_server = 'https://%s' % self._gerrit_host
1718 return self._gerrit_server
1719
1720 @staticmethod
1721 def IssueSetting(branch):
1722 return 'branch.%s.gerritissue' % branch
1723
1724 def PatchsetSetting(self):
1725 """Return the git setting that stores this change's most recent patchset."""
1726 return 'branch.%s.gerritpatchset' % self.GetBranch()
1727
1728 def GetCodereviewServerSetting(self):
1729 """Returns the git setting that stores this change's Gerrit server."""
1730 branch = self.GetBranch()
1731 if branch:
1732 return 'branch.%s.gerritserver' % branch
1733 return None
1734
1735 def GetRieveldObjForPresubmit(self):
1736 class ThisIsNotRietveldIssue(object):
1737 def __nonzero__(self):
1738 # This is a hack to make presubmit_support think that rietveld is not
1739 # defined, yet still ensure that calls directly result in a decent
1740 # exception message below.
1741 return False
1742
1743 def __getattr__(self, attr):
1744 print(
1745 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1746 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1747 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1748 'or use Rietveld for codereview.\n'
1749 'See also http://crbug.com/579160.' % attr)
1750 raise NotImplementedError()
1751 return ThisIsNotRietveldIssue()
1752
1753 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001754 """Apply a rough heuristic to give a simple summary of an issue's review
1755 or CQ status, assuming adherence to a common workflow.
1756
1757 Returns None if no issue for this branch, or one of the following keywords:
1758 * 'error' - error from review tool (including deleted issues)
1759 * 'unsent' - no reviewers added
1760 * 'waiting' - waiting for review
1761 * 'reply' - waiting for owner to reply to review
1762 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1763 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1764 * 'commit' - in the commit queue
1765 * 'closed' - abandoned
1766 """
1767 if not self.GetIssue():
1768 return None
1769
1770 try:
1771 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1772 except httplib.HTTPException:
1773 return 'error'
1774
1775 if data['status'] == 'ABANDONED':
1776 return 'closed'
1777
1778 cq_label = data['labels'].get('Commit-Queue', {})
1779 if cq_label:
1780 # Vote value is a stringified integer, which we expect from 0 to 2.
1781 vote_value = cq_label.get('value', '0')
1782 vote_text = cq_label.get('values', {}).get(vote_value, '')
1783 if vote_text.lower() == 'commit':
1784 return 'commit'
1785
1786 lgtm_label = data['labels'].get('Code-Review', {})
1787 if lgtm_label:
1788 if 'rejected' in lgtm_label:
1789 return 'not lgtm'
1790 if 'approved' in lgtm_label:
1791 return 'lgtm'
1792
1793 if not data.get('reviewers', {}).get('REVIEWER', []):
1794 return 'unsent'
1795
1796 messages = data.get('messages', [])
1797 if messages:
1798 owner = data['owner'].get('_account_id')
1799 last_message_author = messages[-1].get('author', {}).get('_account_id')
1800 if owner != last_message_author:
1801 # Some reply from non-owner.
1802 return 'reply'
1803
1804 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001805
1806 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001807 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 return data['revisions'][data['current_revision']]['_number']
1809
1810 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001811 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001812 return data['revisions'][data['current_revision']]['commit_with_footers']
1813
1814 def UpdateDescriptionRemote(self, description):
1815 # TODO(tandrii)
1816 raise NotImplementedError()
1817
1818 def CloseIssue(self):
1819 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1820
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001821 def SubmitIssue(self, wait_for_merge=True):
1822 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
1823 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001825 def _GetChangeDetail(self, options):
1826 return gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1827 options)
1828
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001829 def CMDLand(self, force, bypass_hooks, verbose):
1830 if git_common.is_dirty_git_tree('land'):
1831 return 1
1832 differs = True
1833 last_upload = RunGit(['config',
1834 'branch.%s.gerritsquashhash' % self.GetBranch()],
1835 error_ok=True).strip()
1836 # Note: git diff outputs nothing if there is no diff.
1837 if not last_upload or RunGit(['diff', last_upload]).strip():
1838 print('WARNING: some changes from local branch haven\'t been uploaded')
1839 else:
1840 detail = self._GetChangeDetail(['CURRENT_REVISION'])
1841 if detail['current_revision'] == last_upload:
1842 differs = False
1843 else:
1844 print('WARNING: local branch contents differ from latest uploaded '
1845 'patchset')
1846 if differs:
1847 if not force:
1848 ask_for_data(
1849 'Do you want to submit latest Gerrit patchset and bypass hooks?')
1850 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
1851 elif not bypass_hooks:
1852 hook_results = self.RunHook(
1853 committing=True,
1854 may_prompt=not force,
1855 verbose=verbose,
1856 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
1857 if not hook_results.should_continue():
1858 return 1
1859
1860 self.SubmitIssue(wait_for_merge=True)
1861 print('Issue %s has been submitted.' % self.GetIssueURL())
1862 return 0
1863
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001864 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1865 directory):
1866 assert not reject
1867 assert not nocommit
1868 assert not directory
1869 assert parsed_issue_arg.valid
1870
1871 self._changelist.issue = parsed_issue_arg.issue
1872
1873 if parsed_issue_arg.hostname:
1874 self._gerrit_host = parsed_issue_arg.hostname
1875 self._gerrit_server = 'https://%s' % self._gerrit_host
1876
1877 detail = self._GetChangeDetail(['ALL_REVISIONS'])
1878
1879 if not parsed_issue_arg.patchset:
1880 # Use current revision by default.
1881 revision_info = detail['revisions'][detail['current_revision']]
1882 patchset = int(revision_info['_number'])
1883 else:
1884 patchset = parsed_issue_arg.patchset
1885 for revision_info in detail['revisions'].itervalues():
1886 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1887 break
1888 else:
1889 DieWithError('Couldn\'t find patchset %i in issue %i' %
1890 (parsed_issue_arg.patchset, self.GetIssue()))
1891
1892 fetch_info = revision_info['fetch']['http']
1893 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
1894 RunGit(['cherry-pick', 'FETCH_HEAD'])
1895 self.SetIssue(self.GetIssue())
1896 self.SetPatchset(patchset)
1897 print('Committed patch for issue %i pathset %i locally' %
1898 (self.GetIssue(), self.GetPatchset()))
1899 return 0
1900
1901 @staticmethod
1902 def ParseIssueURL(parsed_url):
1903 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1904 return None
1905 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
1906 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
1907 # Short urls like https://domain/<issue_number> can be used, but don't allow
1908 # specifying the patchset (you'd 404), but we allow that here.
1909 if parsed_url.path == '/':
1910 part = parsed_url.fragment
1911 else:
1912 part = parsed_url.path
1913 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
1914 if match:
1915 return _ParsedIssueNumberArgument(
1916 issue=int(match.group(2)),
1917 patchset=int(match.group(4)) if match.group(4) else None,
1918 hostname=parsed_url.netloc)
1919 return None
1920
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001921
1922_CODEREVIEW_IMPLEMENTATIONS = {
1923 'rietveld': _RietveldChangelistImpl,
1924 'gerrit': _GerritChangelistImpl,
1925}
1926
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001927
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001928class ChangeDescription(object):
1929 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001930 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001931 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001932
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001933 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001934 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001935
agable@chromium.org42c20792013-09-12 17:34:49 +00001936 @property # www.logilab.org/ticket/89786
1937 def description(self): # pylint: disable=E0202
1938 return '\n'.join(self._description_lines)
1939
1940 def set_description(self, desc):
1941 if isinstance(desc, basestring):
1942 lines = desc.splitlines()
1943 else:
1944 lines = [line.rstrip() for line in desc]
1945 while lines and not lines[0]:
1946 lines.pop(0)
1947 while lines and not lines[-1]:
1948 lines.pop(-1)
1949 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001950
piman@chromium.org336f9122014-09-04 02:16:55 +00001951 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001952 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001953 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001954 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001955 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001956 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001957
agable@chromium.org42c20792013-09-12 17:34:49 +00001958 # Get the set of R= and TBR= lines and remove them from the desciption.
1959 regexp = re.compile(self.R_LINE)
1960 matches = [regexp.match(line) for line in self._description_lines]
1961 new_desc = [l for i, l in enumerate(self._description_lines)
1962 if not matches[i]]
1963 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001964
agable@chromium.org42c20792013-09-12 17:34:49 +00001965 # Construct new unified R= and TBR= lines.
1966 r_names = []
1967 tbr_names = []
1968 for match in matches:
1969 if not match:
1970 continue
1971 people = cleanup_list([match.group(2).strip()])
1972 if match.group(1) == 'TBR':
1973 tbr_names.extend(people)
1974 else:
1975 r_names.extend(people)
1976 for name in r_names:
1977 if name not in reviewers:
1978 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001979 if add_owners_tbr:
1980 owners_db = owners.Database(change.RepositoryRoot(),
1981 fopen=file, os_path=os.path, glob=glob.glob)
1982 all_reviewers = set(tbr_names + reviewers)
1983 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1984 all_reviewers)
1985 tbr_names.extend(owners_db.reviewers_for(missing_files,
1986 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001987 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1988 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1989
1990 # Put the new lines in the description where the old first R= line was.
1991 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1992 if 0 <= line_loc < len(self._description_lines):
1993 if new_tbr_line:
1994 self._description_lines.insert(line_loc, new_tbr_line)
1995 if new_r_line:
1996 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001997 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001998 if new_r_line:
1999 self.append_footer(new_r_line)
2000 if new_tbr_line:
2001 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002002
2003 def prompt(self):
2004 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002005 self.set_description([
2006 '# Enter a description of the change.',
2007 '# This will be displayed on the codereview site.',
2008 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002009 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002010 '--------------------',
2011 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002012
agable@chromium.org42c20792013-09-12 17:34:49 +00002013 regexp = re.compile(self.BUG_LINE)
2014 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002015 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002016 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002017 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002018 if not content:
2019 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002020 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002021
2022 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002023 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2024 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002025 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002026 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002027
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002028 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002029 if self._description_lines:
2030 # Add an empty line if either the last line or the new line isn't a tag.
2031 last_line = self._description_lines[-1]
2032 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2033 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2034 self._description_lines.append('')
2035 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002036
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002037 def get_reviewers(self):
2038 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002039 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2040 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002041 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002042
2043
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002044def get_approving_reviewers(props):
2045 """Retrieves the reviewers that approved a CL from the issue properties with
2046 messages.
2047
2048 Note that the list may contain reviewers that are not committer, thus are not
2049 considered by the CQ.
2050 """
2051 return sorted(
2052 set(
2053 message['sender']
2054 for message in props['messages']
2055 if message['approval'] and message['sender'] in props['reviewers']
2056 )
2057 )
2058
2059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060def FindCodereviewSettingsFile(filename='codereview.settings'):
2061 """Finds the given file starting in the cwd and going up.
2062
2063 Only looks up to the top of the repository unless an
2064 'inherit-review-settings-ok' file exists in the root of the repository.
2065 """
2066 inherit_ok_file = 'inherit-review-settings-ok'
2067 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002068 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002069 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2070 root = '/'
2071 while True:
2072 if filename in os.listdir(cwd):
2073 if os.path.isfile(os.path.join(cwd, filename)):
2074 return open(os.path.join(cwd, filename))
2075 if cwd == root:
2076 break
2077 cwd = os.path.dirname(cwd)
2078
2079
2080def LoadCodereviewSettingsFromFile(fileobj):
2081 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002082 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084 def SetProperty(name, setting, unset_error_ok=False):
2085 fullname = 'rietveld.' + name
2086 if setting in keyvals:
2087 RunGit(['config', fullname, keyvals[setting]])
2088 else:
2089 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2090
2091 SetProperty('server', 'CODE_REVIEW_SERVER')
2092 # Only server setting is required. Other settings can be absent.
2093 # In that case, we ignore errors raised during option deletion attempt.
2094 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002095 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2097 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002098 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002099 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002100 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2101 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002102 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002103 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002104 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002105 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2106 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002108 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002109 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002110
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002111 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2112 RunGit(['config', 'gerrit.squash-uploads',
2113 keyvals['GERRIT_SQUASH_UPLOADS']])
2114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2116 #should be of the form
2117 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2118 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2119 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2120 keyvals['ORIGIN_URL_CONFIG']])
2121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002123def urlretrieve(source, destination):
2124 """urllib is broken for SSL connections via a proxy therefore we
2125 can't use urllib.urlretrieve()."""
2126 with open(destination, 'w') as f:
2127 f.write(urllib2.urlopen(source).read())
2128
2129
ukai@chromium.org712d6102013-11-27 00:52:58 +00002130def hasSheBang(fname):
2131 """Checks fname is a #! script."""
2132 with open(fname) as f:
2133 return f.read(2).startswith('#!')
2134
2135
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002136# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2137def DownloadHooks(*args, **kwargs):
2138 pass
2139
2140
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002141def DownloadGerritHook(force):
2142 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002143
2144 Args:
2145 force: True to update hooks. False to install hooks if not present.
2146 """
2147 if not settings.GetIsGerrit():
2148 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002149 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002150 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2151 if not os.access(dst, os.X_OK):
2152 if os.path.exists(dst):
2153 if not force:
2154 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002155 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002156 print(
2157 'WARNING: installing Gerrit commit-msg hook.\n'
2158 ' This behavior of git cl will soon be disabled.\n'
2159 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002160 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002161 if not hasSheBang(dst):
2162 DieWithError('Not a script: %s\n'
2163 'You need to download from\n%s\n'
2164 'into .git/hooks/commit-msg and '
2165 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002166 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2167 except Exception:
2168 if os.path.exists(dst):
2169 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002170 DieWithError('\nFailed to download hooks.\n'
2171 'You need to download from\n%s\n'
2172 'into .git/hooks/commit-msg and '
2173 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002174
2175
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002176
2177def GetRietveldCodereviewSettingsInteractively():
2178 """Prompt the user for settings."""
2179 server = settings.GetDefaultServerUrl(error_ok=True)
2180 prompt = 'Rietveld server (host[:port])'
2181 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2182 newserver = ask_for_data(prompt + ':')
2183 if not server and not newserver:
2184 newserver = DEFAULT_SERVER
2185 if newserver:
2186 newserver = gclient_utils.UpgradeToHttps(newserver)
2187 if newserver != server:
2188 RunGit(['config', 'rietveld.server', newserver])
2189
2190 def SetProperty(initial, caption, name, is_url):
2191 prompt = caption
2192 if initial:
2193 prompt += ' ("x" to clear) [%s]' % initial
2194 new_val = ask_for_data(prompt + ':')
2195 if new_val == 'x':
2196 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2197 elif new_val:
2198 if is_url:
2199 new_val = gclient_utils.UpgradeToHttps(new_val)
2200 if new_val != initial:
2201 RunGit(['config', 'rietveld.' + name, new_val])
2202
2203 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2204 SetProperty(settings.GetDefaultPrivateFlag(),
2205 'Private flag (rietveld only)', 'private', False)
2206 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2207 'tree-status-url', False)
2208 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2209 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2210 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2211 'run-post-upload-hook', False)
2212
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002213@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002214def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002215 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002216
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002217 print('WARNING: git cl config works for Rietveld only.\n'
2218 'For Gerrit, see http://crbug.com/579160.')
2219 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002220 parser.add_option('--activate-update', action='store_true',
2221 help='activate auto-updating [rietveld] section in '
2222 '.git/config')
2223 parser.add_option('--deactivate-update', action='store_true',
2224 help='deactivate auto-updating [rietveld] section in '
2225 '.git/config')
2226 options, args = parser.parse_args(args)
2227
2228 if options.deactivate_update:
2229 RunGit(['config', 'rietveld.autoupdate', 'false'])
2230 return
2231
2232 if options.activate_update:
2233 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2234 return
2235
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002236 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002237 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002238 return 0
2239
2240 url = args[0]
2241 if not url.endswith('codereview.settings'):
2242 url = os.path.join(url, 'codereview.settings')
2243
2244 # Load code review settings and download hooks (if available).
2245 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2246 return 0
2247
2248
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002249def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002250 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002251 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2252 branch = ShortBranchName(branchref)
2253 _, args = parser.parse_args(args)
2254 if not args:
2255 print("Current base-url:")
2256 return RunGit(['config', 'branch.%s.base-url' % branch],
2257 error_ok=False).strip()
2258 else:
2259 print("Setting base-url to %s" % args[0])
2260 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2261 error_ok=False).strip()
2262
2263
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002264def color_for_status(status):
2265 """Maps a Changelist status to color, for CMDstatus and other tools."""
2266 return {
2267 'unsent': Fore.RED,
2268 'waiting': Fore.BLUE,
2269 'reply': Fore.YELLOW,
2270 'lgtm': Fore.GREEN,
2271 'commit': Fore.MAGENTA,
2272 'closed': Fore.CYAN,
2273 'error': Fore.WHITE,
2274 }.get(status, Fore.WHITE)
2275
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002276def fetch_cl_status(branch, auth_config=None):
2277 """Fetches information for an issue and returns (branch, issue, status)."""
2278 cl = Changelist(branchref=branch, auth_config=auth_config)
2279 url = cl.GetIssueURL()
2280 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002281
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002282 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002283 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002284 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002285
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002286 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002287
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002288def get_cl_statuses(
2289 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002290 """Returns a blocking iterable of (branch, issue, color) for given branches.
2291
2292 If fine_grained is true, this will fetch CL statuses from the server.
2293 Otherwise, simply indicate if there's a matching url for the given branches.
2294
2295 If max_processes is specified, it is used as the maximum number of processes
2296 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2297 spawned.
2298 """
2299 # Silence upload.py otherwise it becomes unwieldly.
2300 upload.verbosity = 0
2301
2302 if fine_grained:
2303 # Process one branch synchronously to work through authentication, then
2304 # spawn processes to process all the other branches in parallel.
2305 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002306 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2307 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002308
2309 branches_to_fetch = branches[1:]
2310 pool = ThreadPool(
2311 min(max_processes, len(branches_to_fetch))
2312 if max_processes is not None
2313 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002314 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002315 yield x
2316 else:
2317 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2318 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002319 cl = Changelist(branchref=b, auth_config=auth_config)
2320 url = cl.GetIssueURL()
2321 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002322
rmistry@google.com2dd99862015-06-22 12:22:18 +00002323
2324def upload_branch_deps(cl, args):
2325 """Uploads CLs of local branches that are dependents of the current branch.
2326
2327 If the local branch dependency tree looks like:
2328 test1 -> test2.1 -> test3.1
2329 -> test3.2
2330 -> test2.2 -> test3.3
2331
2332 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2333 run on the dependent branches in this order:
2334 test2.1, test3.1, test3.2, test2.2, test3.3
2335
2336 Note: This function does not rebase your local dependent branches. Use it when
2337 you make a change to the parent branch that will not conflict with its
2338 dependent branches, and you would like their dependencies updated in
2339 Rietveld.
2340 """
2341 if git_common.is_dirty_git_tree('upload-branch-deps'):
2342 return 1
2343
2344 root_branch = cl.GetBranch()
2345 if root_branch is None:
2346 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2347 'Get on a branch!')
2348 if not cl.GetIssue() or not cl.GetPatchset():
2349 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2350 'patchset dependencies without an uploaded CL.')
2351
2352 branches = RunGit(['for-each-ref',
2353 '--format=%(refname:short) %(upstream:short)',
2354 'refs/heads'])
2355 if not branches:
2356 print('No local branches found.')
2357 return 0
2358
2359 # Create a dictionary of all local branches to the branches that are dependent
2360 # on it.
2361 tracked_to_dependents = collections.defaultdict(list)
2362 for b in branches.splitlines():
2363 tokens = b.split()
2364 if len(tokens) == 2:
2365 branch_name, tracked = tokens
2366 tracked_to_dependents[tracked].append(branch_name)
2367
2368 print
2369 print 'The dependent local branches of %s are:' % root_branch
2370 dependents = []
2371 def traverse_dependents_preorder(branch, padding=''):
2372 dependents_to_process = tracked_to_dependents.get(branch, [])
2373 padding += ' '
2374 for dependent in dependents_to_process:
2375 print '%s%s' % (padding, dependent)
2376 dependents.append(dependent)
2377 traverse_dependents_preorder(dependent, padding)
2378 traverse_dependents_preorder(root_branch)
2379 print
2380
2381 if not dependents:
2382 print 'There are no dependent local branches for %s' % root_branch
2383 return 0
2384
2385 print ('This command will checkout all dependent branches and run '
2386 '"git cl upload".')
2387 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2388
andybons@chromium.org962f9462016-02-03 20:00:42 +00002389 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002390 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002391 args.extend(['-t', 'Updated patchset dependency'])
2392
rmistry@google.com2dd99862015-06-22 12:22:18 +00002393 # Record all dependents that failed to upload.
2394 failures = {}
2395 # Go through all dependents, checkout the branch and upload.
2396 try:
2397 for dependent_branch in dependents:
2398 print
2399 print '--------------------------------------'
2400 print 'Running "git cl upload" from %s:' % dependent_branch
2401 RunGit(['checkout', '-q', dependent_branch])
2402 print
2403 try:
2404 if CMDupload(OptionParser(), args) != 0:
2405 print 'Upload failed for %s!' % dependent_branch
2406 failures[dependent_branch] = 1
2407 except: # pylint: disable=W0702
2408 failures[dependent_branch] = 1
2409 print
2410 finally:
2411 # Swap back to the original root branch.
2412 RunGit(['checkout', '-q', root_branch])
2413
2414 print
2415 print 'Upload complete for dependent branches!'
2416 for dependent_branch in dependents:
2417 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2418 print ' %s : %s' % (dependent_branch, upload_status)
2419 print
2420
2421 return 0
2422
2423
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002424def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002425 """Show status of changelists.
2426
2427 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002428 - Red not sent for review or broken
2429 - Blue waiting for review
2430 - Yellow waiting for you to reply to review
2431 - Green LGTM'ed
2432 - Magenta in the commit queue
2433 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002434
2435 Also see 'git cl comments'.
2436 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002437 parser.add_option('--field',
2438 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002439 parser.add_option('-f', '--fast', action='store_true',
2440 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002441 parser.add_option(
2442 '-j', '--maxjobs', action='store', type=int,
2443 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002444
2445 auth.add_auth_options(parser)
2446 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002447 if args:
2448 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002449 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002451 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002452 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002453 if options.field.startswith('desc'):
2454 print cl.GetDescription()
2455 elif options.field == 'id':
2456 issueid = cl.GetIssue()
2457 if issueid:
2458 print issueid
2459 elif options.field == 'patch':
2460 patchset = cl.GetPatchset()
2461 if patchset:
2462 print patchset
2463 elif options.field == 'url':
2464 url = cl.GetIssueURL()
2465 if url:
2466 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002467 return 0
2468
2469 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2470 if not branches:
2471 print('No local branch found.')
2472 return 0
2473
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002474 changes = (
2475 Changelist(branchref=b, auth_config=auth_config)
2476 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002477 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002478 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002479 alignment = max(5, max(len(b) for b in branches))
2480 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002481 output = get_cl_statuses(branches,
2482 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002483 max_processes=options.maxjobs,
2484 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002485
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002486 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002487 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002488 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002489 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002490 b, i, status = output.next()
2491 branch_statuses[b] = (i, status)
2492 issue_url, status = branch_statuses.pop(branch)
2493 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002494 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00002495 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00002496 color = ''
2497 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002498 status_str = '(%s)' % status if status else ''
2499 print ' %*s : %s%s %s%s' % (
2500 alignment, ShortBranchName(branch), color, issue_url, status_str,
2501 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002502
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002503 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002504 print
2505 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002506 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002507 if not cl.GetIssue():
2508 print 'No issue assigned.'
2509 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002510 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002511 if not options.fast:
2512 print 'Issue description:'
2513 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002514 return 0
2515
2516
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002517def colorize_CMDstatus_doc():
2518 """To be called once in main() to add colors to git cl status help."""
2519 colors = [i for i in dir(Fore) if i[0].isupper()]
2520
2521 def colorize_line(line):
2522 for color in colors:
2523 if color in line.upper():
2524 # Extract whitespaces first and the leading '-'.
2525 indent = len(line) - len(line.lstrip(' ')) + 1
2526 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2527 return line
2528
2529 lines = CMDstatus.__doc__.splitlines()
2530 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2531
2532
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002533@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002534def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002535 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002536
2537 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002538 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002539 parser.add_option('-r', '--reverse', action='store_true',
2540 help='Lookup the branch(es) for the specified issues. If '
2541 'no issues are specified, all branches with mapped '
2542 'issues will be listed.')
2543 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002544
dnj@chromium.org406c4402015-03-03 17:22:28 +00002545 if options.reverse:
2546 branches = RunGit(['for-each-ref', 'refs/heads',
2547 '--format=%(refname:short)']).splitlines()
2548
2549 # Reverse issue lookup.
2550 issue_branch_map = {}
2551 for branch in branches:
2552 cl = Changelist(branchref=branch)
2553 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2554 if not args:
2555 args = sorted(issue_branch_map.iterkeys())
2556 for issue in args:
2557 if not issue:
2558 continue
2559 print 'Branch for issue number %s: %s' % (
2560 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2561 else:
2562 cl = Changelist()
2563 if len(args) > 0:
2564 try:
2565 issue = int(args[0])
2566 except ValueError:
2567 DieWithError('Pass a number to set the issue or none to list it.\n'
2568 'Maybe you want to run git cl status?')
2569 cl.SetIssue(issue)
2570 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002571 return 0
2572
2573
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002574def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002575 """Shows or posts review comments for any changelist."""
2576 parser.add_option('-a', '--add-comment', dest='comment',
2577 help='comment to add to an issue')
2578 parser.add_option('-i', dest='issue',
2579 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002580 parser.add_option('-j', '--json-file',
2581 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002582 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002583 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002584 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002585
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002586 issue = None
2587 if options.issue:
2588 try:
2589 issue = int(options.issue)
2590 except ValueError:
2591 DieWithError('A review issue id is expected to be a number')
2592
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002593 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002594
2595 if options.comment:
2596 cl.AddComment(options.comment)
2597 return 0
2598
2599 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002600 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002601 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002602 summary.append({
2603 'date': message['date'],
2604 'lgtm': False,
2605 'message': message['text'],
2606 'not_lgtm': False,
2607 'sender': message['sender'],
2608 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002609 if message['disapproval']:
2610 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002611 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002612 elif message['approval']:
2613 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002614 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002615 elif message['sender'] == data['owner_email']:
2616 color = Fore.MAGENTA
2617 else:
2618 color = Fore.BLUE
2619 print '\n%s%s %s%s' % (
2620 color, message['date'].split('.', 1)[0], message['sender'],
2621 Fore.RESET)
2622 if message['text'].strip():
2623 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002624 if options.json_file:
2625 with open(options.json_file, 'wb') as f:
2626 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002627 return 0
2628
2629
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002630def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002631 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002632 parser.add_option('-d', '--display', action='store_true',
2633 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002634 auth.add_auth_options(parser)
2635 options, _ = parser.parse_args(args)
2636 auth_config = auth.extract_auth_config_from_options(options)
2637 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002638 if not cl.GetIssue():
2639 DieWithError('This branch has no associated changelist.')
2640 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002641 if options.display:
2642 print description.description
2643 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002644 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002645 if cl.GetDescription() != description.description:
2646 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002647 return 0
2648
2649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002650def CreateDescriptionFromLog(args):
2651 """Pulls out the commit log to use as a base for the CL description."""
2652 log_args = []
2653 if len(args) == 1 and not args[0].endswith('.'):
2654 log_args = [args[0] + '..']
2655 elif len(args) == 1 and args[0].endswith('...'):
2656 log_args = [args[0][:-1]]
2657 elif len(args) == 2:
2658 log_args = [args[0] + '..' + args[1]]
2659 else:
2660 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002661 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002662
2663
thestig@chromium.org44202a22014-03-11 19:22:18 +00002664def CMDlint(parser, args):
2665 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002666 parser.add_option('--filter', action='append', metavar='-x,+y',
2667 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002668 auth.add_auth_options(parser)
2669 options, args = parser.parse_args(args)
2670 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002671
2672 # Access to a protected member _XX of a client class
2673 # pylint: disable=W0212
2674 try:
2675 import cpplint
2676 import cpplint_chromium
2677 except ImportError:
2678 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2679 return 1
2680
2681 # Change the current working directory before calling lint so that it
2682 # shows the correct base.
2683 previous_cwd = os.getcwd()
2684 os.chdir(settings.GetRoot())
2685 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002686 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002687 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2688 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002689 if not files:
2690 print "Cannot lint an empty CL"
2691 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002692
2693 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002694 command = args + files
2695 if options.filter:
2696 command = ['--filter=' + ','.join(options.filter)] + command
2697 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002698
2699 white_regex = re.compile(settings.GetLintRegex())
2700 black_regex = re.compile(settings.GetLintIgnoreRegex())
2701 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2702 for filename in filenames:
2703 if white_regex.match(filename):
2704 if black_regex.match(filename):
2705 print "Ignoring file %s" % filename
2706 else:
2707 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2708 extra_check_functions)
2709 else:
2710 print "Skipping file %s" % filename
2711 finally:
2712 os.chdir(previous_cwd)
2713 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2714 if cpplint._cpplint_state.error_count != 0:
2715 return 1
2716 return 0
2717
2718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002719def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002720 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002721 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002722 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002723 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002724 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002725 auth.add_auth_options(parser)
2726 options, args = parser.parse_args(args)
2727 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002728
sbc@chromium.org71437c02015-04-09 19:29:40 +00002729 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002730 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731 return 1
2732
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002733 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002734 if args:
2735 base_branch = args[0]
2736 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002737 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002738 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002739
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002740 cl.RunHook(
2741 committing=not options.upload,
2742 may_prompt=False,
2743 verbose=options.verbose,
2744 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002745 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746
2747
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002748def AddChangeIdToCommitMessage(options, args):
2749 """Re-commits using the current message, assumes the commit hook is in
2750 place.
2751 """
2752 log_desc = options.message or CreateDescriptionFromLog(args)
2753 git_command = ['commit', '--amend', '-m', log_desc]
2754 RunGit(git_command)
2755 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002756 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002757 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002758 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002759 else:
2760 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2761
2762
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002763def GenerateGerritChangeId(message):
2764 """Returns Ixxxxxx...xxx change id.
2765
2766 Works the same way as
2767 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2768 but can be called on demand on all platforms.
2769
2770 The basic idea is to generate git hash of a state of the tree, original commit
2771 message, author/committer info and timestamps.
2772 """
2773 lines = []
2774 tree_hash = RunGitSilent(['write-tree'])
2775 lines.append('tree %s' % tree_hash.strip())
2776 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2777 if code == 0:
2778 lines.append('parent %s' % parent.strip())
2779 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2780 lines.append('author %s' % author.strip())
2781 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2782 lines.append('committer %s' % committer.strip())
2783 lines.append('')
2784 # Note: Gerrit's commit-hook actually cleans message of some lines and
2785 # whitespace. This code is not doing this, but it clearly won't decrease
2786 # entropy.
2787 lines.append(message)
2788 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2789 stdin='\n'.join(lines))
2790 return 'I%s' % change_hash.strip()
2791
2792
piman@chromium.org336f9122014-09-04 02:16:55 +00002793def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002794 """upload the current branch to gerrit."""
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002795 # TODO(tandrii): refactor this to be a method of _GerritChangelistImpl,
2796 # to avoid private members accessors below.
2797
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002798 # We assume the remote called "origin" is the one we want.
2799 # It is probably not worthwhile to support different workflows.
2800 gerrit_remote = 'origin'
2801
luqui@chromium.org609f3952015-05-04 22:47:04 +00002802 remote, remote_branch = cl.GetRemoteBranch()
2803 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2804 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002805
andybons@chromium.org962f9462016-02-03 20:00:42 +00002806 if options.title:
2807 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002808 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002809
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002810 if options.squash:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002811 if not cl.GetIssue():
2812 # TODO(tandrii): deperecate this after 2016Q2.
2813 # Backwards compatibility with shadow branch, which used to contain
2814 # change-id for a given branch, using which we can fetch actual issue
2815 # number and set it as the property of the branch, which is the new way.
2816 message = RunGitSilent(['show', '--format=%B', '-s',
2817 'refs/heads/git_cl_uploads/%s' % cl.GetBranch()])
2818 if message:
2819 change_ids = git_footers.get_footer_change_id(message.strip())
2820 if change_ids and len(change_ids) == 1:
2821 details = gerrit_util.GetChangeDetail(
2822 cl._codereview_impl._GetGerritHost(), change_ids[0])
2823 if details:
2824 print('WARNING: found old upload in branch git_cl_uploads/%s '
2825 'corresponding to issue %s' %
2826 (cl.GetBranch(), details['_number']))
2827 cl.SetIssue(details['_number'])
2828 if not cl.GetIssue():
2829 DieWithError(
2830 '\n' # For readability of the blob below.
2831 'Found old upload in branch git_cl_uploads/%s, '
2832 'but failed to find corresponding Gerrit issue.\n'
2833 'If you know the issue number, set it manually first:\n'
2834 ' git cl issue 123456\n'
2835 'If you intended to upload this CL as new issue, '
2836 'just delete or rename the old upload branch:\n'
2837 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2838 'After that, please run git cl upload again.' %
2839 tuple([cl.GetBranch()] * 3))
2840 # End of backwards compatability.
2841
2842 if cl.GetIssue():
2843 # Try to get the message from a previous upload.
2844 message = cl.GetDescription()
2845 if not message:
2846 DieWithError(
2847 'failed to fetch description from current Gerrit issue %d\n'
2848 '%s' % (cl.GetIssue(), cl.GetIssueURL()))
2849 change_id = cl._codereview_impl._GetChangeDetail([])['change_id']
2850 while True:
2851 footer_change_ids = git_footers.get_footer_change_id(message)
2852 if footer_change_ids == [change_id]:
2853 break
2854 if not footer_change_ids:
2855 message = git_footers.add_footer_change_id(message, change_id)
2856 print('WARNING: appended missing Change-Id to issue description')
2857 continue
2858 # There is already a valid footer but with different or several ids.
2859 # Doing this automatically is non-trivial as we don't want to lose
2860 # existing other footers, yet we want to append just 1 desired
2861 # Change-Id. Thus, just create a new footer, but let user verify the new
2862 # description.
2863 message = '%s\n\nChange-Id: %s' % (message, change_id)
2864 print(
2865 'WARNING: issue %s has Change-Id footer(s):\n'
2866 ' %s\n'
2867 'but issue has Change-Id %s, according to Gerrit.\n'
2868 'Please, check the proposed correction to the description, '
2869 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2870 % (cl.GetIssue(), '\n '.join(footer_change_ids), change_id,
2871 change_id))
2872 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2873 if not options.force:
2874 change_desc = ChangeDescription(message)
2875 change_desc.prompt()
2876 message = change_desc.description
2877 if not message:
2878 DieWithError("Description is empty. Aborting...")
2879 # Continue the while loop.
2880 # Sanity check of this code - we should end up with proper message footer.
2881 assert [change_id] == git_footers.get_footer_change_id(message)
2882 change_desc = ChangeDescription(message)
2883 else:
2884 change_desc = ChangeDescription(
2885 options.message or CreateDescriptionFromLog(args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002886 if not options.force:
2887 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002888 if not change_desc.description:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002889 DieWithError("Description is empty. Aborting...")
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002890 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002891 change_ids = git_footers.get_footer_change_id(message)
2892 if len(change_ids) > 1:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002893 DieWithError('too many Change-Id footers, at most 1 allowed.')
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002894 if not change_ids:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002895 # Generate the Change-Id automatically.
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002896 message = git_footers.add_footer_change_id(
2897 message, GenerateGerritChangeId(message))
2898 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002899 change_ids = git_footers.get_footer_change_id(message)
2900 assert len(change_ids) == 1
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002901 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002902
2903 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2904 if remote is '.':
2905 # If our upstream branch is local, we base our squashed commit on its
2906 # squashed version.
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002907 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2908 # Check the squashed hash of the parent.
2909 parent = RunGit(['config',
2910 'branch.%s.gerritsquashhash' % upstream_branch_name],
2911 error_ok=True).strip()
2912 # Verify that the upstream branch has been uploaded too, otherwise
2913 # Gerrit will create additional CLs when uploading.
2914 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2915 RunGitSilent(['rev-parse', parent + ':'])):
2916 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2917 DieWithError(
2918 'Upload upstream branch %s first.\n'
2919 'Note: maybe you\'ve uploaded it with --no-squash or with an old\n'
2920 ' version of depot_tools. If so, then re-upload it with:\n'
2921 ' git cl upload --squash\n' % upstream_branch_name)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002922 else:
2923 parent = cl.GetCommonAncestorWithUpstream()
2924
2925 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2926 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2927 '-m', message]).strip()
2928 else:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002929 change_desc = ChangeDescription(
2930 options.message or CreateDescriptionFromLog(args))
2931 if not change_desc.description:
2932 DieWithError("Description is empty. Aborting...")
2933
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002934 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002935 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002936 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002937 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002938 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002939 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002940
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002941 assert change_desc
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002942 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2943 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002944 if len(commits) > 1:
2945 print('WARNING: This will upload %d commits. Run the following command '
2946 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002947 print('git log %s..%s' % (parent, ref_to_push))
2948 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002949 'commit.')
2950 ask_for_data('About to upload; enter to confirm.')
2951
piman@chromium.org336f9122014-09-04 02:16:55 +00002952 if options.reviewers or options.tbr_owners:
2953 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002954
ukai@chromium.orge8077812012-02-03 03:41:46 +00002955 receive_options = []
2956 cc = cl.GetCCList().split(',')
2957 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002958 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002959 cc = filter(None, cc)
2960 if cc:
2961 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002962 if change_desc.get_reviewers():
2963 receive_options.extend(
2964 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965
ukai@chromium.orge8077812012-02-03 03:41:46 +00002966 git_command = ['push']
2967 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002968 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002969 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002970 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002971 push_stdout = gclient_utils.CheckCallAndFilter(
2972 ['git'] + git_command,
2973 print_stdout=True,
2974 # Flush after every line: useful for seeing progress when running as
2975 # recipe.
2976 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002977
2978 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002979 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2980 change_numbers = [m.group(1)
2981 for m in map(regex.match, push_stdout.splitlines())
2982 if m]
2983 if len(change_numbers) != 1:
2984 DieWithError(
2985 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2986 'Change-Id: %s') % (len(change_numbers), change_id))
2987 cl.SetIssue(change_numbers[0])
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002988 RunGit(['config', 'branch.%s.gerritsquashhash' % cl.GetBranch(),
2989 ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002990 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002992
wittman@chromium.org455dc922015-01-26 20:15:50 +00002993def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2994 """Computes the remote branch ref to use for the CL.
2995
2996 Args:
2997 remote (str): The git remote for the CL.
2998 remote_branch (str): The git remote branch for the CL.
2999 target_branch (str): The target branch specified by the user.
3000 pending_prefix (str): The pending prefix from the settings.
3001 """
3002 if not (remote and remote_branch):
3003 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003004
wittman@chromium.org455dc922015-01-26 20:15:50 +00003005 if target_branch:
3006 # Cannonicalize branch references to the equivalent local full symbolic
3007 # refs, which are then translated into the remote full symbolic refs
3008 # below.
3009 if '/' not in target_branch:
3010 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3011 else:
3012 prefix_replacements = (
3013 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3014 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3015 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3016 )
3017 match = None
3018 for regex, replacement in prefix_replacements:
3019 match = re.search(regex, target_branch)
3020 if match:
3021 remote_branch = target_branch.replace(match.group(0), replacement)
3022 break
3023 if not match:
3024 # This is a branch path but not one we recognize; use as-is.
3025 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003026 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3027 # Handle the refs that need to land in different refs.
3028 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003029
wittman@chromium.org455dc922015-01-26 20:15:50 +00003030 # Create the true path to the remote branch.
3031 # Does the following translation:
3032 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3033 # * refs/remotes/origin/master -> refs/heads/master
3034 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3035 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3036 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3037 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3038 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3039 'refs/heads/')
3040 elif remote_branch.startswith('refs/remotes/branch-heads'):
3041 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3042 # If a pending prefix exists then replace refs/ with it.
3043 if pending_prefix:
3044 remote_branch = remote_branch.replace('refs/', pending_prefix)
3045 return remote_branch
3046
3047
piman@chromium.org336f9122014-09-04 02:16:55 +00003048def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003049 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003050 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003051 upload_args.extend(['--server', cl.GetCodereviewServer()])
3052 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
3053 upload_args.extend(auth.auth_config_to_command_options(
3054 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003055 if options.emulate_svn_auto_props:
3056 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003057
3058 change_desc = None
3059
pgervais@chromium.org91141372014-01-09 23:27:20 +00003060 if options.email is not None:
3061 upload_args.extend(['--email', options.email])
3062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003063 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003064 if options.title:
3065 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00003066 if options.message:
3067 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00003068 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003069 print ("This branch is associated with issue %s. "
3070 "Adding patch to that issue." % cl.GetIssue())
3071 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003072 if options.title:
3073 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00003074 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00003076 if options.reviewers or options.tbr_owners:
3077 change_desc.update_reviewers(options.reviewers,
3078 options.tbr_owners,
3079 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003080 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003081 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003082
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003084 print "Description is empty; aborting."
3085 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003086
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003087 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003088 if change_desc.get_reviewers():
3089 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00003090 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003091 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00003092 DieWithError("Must specify reviewers to send email.")
3093 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00003094
3095 # We check this before applying rietveld.private assuming that in
3096 # rietveld.cc only addresses which we can send private CLs to are listed
3097 # if rietveld.private is set, and so we should ignore rietveld.cc only when
3098 # --private is specified explicitly on the command line.
3099 if options.private:
3100 logging.warn('rietveld.cc is ignored since private flag is specified. '
3101 'You need to review and add them manually if necessary.')
3102 cc = cl.GetCCListWithoutDefault()
3103 else:
3104 cc = cl.GetCCList()
3105 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00003106 if cc:
3107 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003108
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003109 if options.private or settings.GetDefaultPrivateFlag() == "True":
3110 upload_args.append('--private')
3111
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003112 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00003113 if not options.find_copies:
3114 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003115
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003116 # Include the upstream repo's URL in the change -- this is useful for
3117 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003118 remote_url = cl.GetGitBaseUrlFromConfig()
3119 if not remote_url:
3120 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003121 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003122 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00003123 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
3124 remote_url = (cl.GetRemoteUrl() + '@'
3125 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003126 if remote_url:
3127 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00003128 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00003129 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
3130 settings.GetPendingRefPrefix())
3131 if target_ref:
3132 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003133
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003134 # Look for dependent patchsets. See crbug.com/480453 for more details.
3135 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3136 upstream_branch = ShortBranchName(upstream_branch)
3137 if remote is '.':
3138 # A local branch is being tracked.
3139 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00003140 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003141 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003142 print ('Skipping dependency patchset upload because git config '
3143 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003144 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003145 else:
3146 auth_config = auth.extract_auth_config_from_options(options)
3147 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
3148 branch_cl_issue_url = branch_cl.GetIssueURL()
3149 branch_cl_issue = branch_cl.GetIssue()
3150 branch_cl_patchset = branch_cl.GetPatchset()
3151 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
3152 upload_args.extend(
3153 ['--depends_on_patchset', '%s:%s' % (
3154 branch_cl_issue, branch_cl_patchset)])
3155 print
3156 print ('The current branch (%s) is tracking a local branch (%s) with '
3157 'an associated CL.') % (cl.GetBranch(), local_branch)
3158 print 'Adding %s/#ps%s as a dependency patchset.' % (
3159 branch_cl_issue_url, branch_cl_patchset)
3160 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003161
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003162 project = settings.GetProject()
3163 if project:
3164 upload_args.extend(['--project', project])
3165
rmistry@google.comef966222015-04-07 11:15:01 +00003166 if options.cq_dry_run:
3167 upload_args.extend(['--cq_dry_run'])
3168
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003169 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00003170 upload_args = ['upload'] + upload_args + args
3171 logging.info('upload.RealMain(%s)', upload_args)
3172 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00003173 issue = int(issue)
3174 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00003175 except KeyboardInterrupt:
3176 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003177 except:
3178 # If we got an exception after the user typed a description for their
3179 # change, back up the description before re-raising.
3180 if change_desc:
3181 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
3182 print '\nGot exception while uploading -- saving description to %s\n' \
3183 % backup_path
3184 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003185 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003186 backup_file.close()
3187 raise
3188
3189 if not cl.GetIssue():
3190 cl.SetIssue(issue)
3191 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003192
3193 if options.use_commit_queue:
3194 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003195 return 0
3196
3197
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003198def cleanup_list(l):
3199 """Fixes a list so that comma separated items are put as individual items.
3200
3201 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3202 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3203 """
3204 items = sum((i.split(',') for i in l), [])
3205 stripped_items = (i.strip() for i in items)
3206 return sorted(filter(None, stripped_items))
3207
3208
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003209@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003210def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003211 """Uploads the current changelist to codereview.
3212
3213 Can skip dependency patchset uploads for a branch by running:
3214 git config branch.branch_name.skip-deps-uploads True
3215 To unset run:
3216 git config --unset branch.branch_name.skip-deps-uploads
3217 Can also set the above globally by using the --global flag.
3218 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003219 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3220 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003221 parser.add_option('--bypass-watchlists', action='store_true',
3222 dest='bypass_watchlists',
3223 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003224 parser.add_option('-f', action='store_true', dest='force',
3225 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003226 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003227 parser.add_option('-t', dest='title',
3228 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003229 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003230 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003231 help='reviewer email addresses')
3232 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003233 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003234 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003235 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003236 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003237 parser.add_option('--emulate_svn_auto_props',
3238 '--emulate-svn-auto-props',
3239 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003240 dest="emulate_svn_auto_props",
3241 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003242 parser.add_option('-c', '--use-commit-queue', action='store_true',
3243 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003244 parser.add_option('--private', action='store_true',
3245 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003246 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003247 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003248 metavar='TARGET',
3249 help='Apply CL to remote ref TARGET. ' +
3250 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003251 parser.add_option('--squash', action='store_true',
3252 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003253 parser.add_option('--no-squash', action='store_true',
3254 help='Don\'t squash multiple commits into one ' +
3255 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003256 parser.add_option('--email', default=None,
3257 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003258 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3259 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003260 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3261 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003262 help='Send the patchset to do a CQ dry run right after '
3263 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003264 parser.add_option('--dependencies', action='store_true',
3265 help='Uploads CLs of all the local branches that depend on '
3266 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003267
rmistry@google.com2dd99862015-06-22 12:22:18 +00003268 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003269 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003270 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003271 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003272 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003273
sbc@chromium.org71437c02015-04-09 19:29:40 +00003274 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003275 return 1
3276
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003277 options.reviewers = cleanup_list(options.reviewers)
3278 options.cc = cleanup_list(options.cc)
3279
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003280 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3281 settings.GetIsGerrit()
3282
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003283 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003284 if args:
3285 # TODO(ukai): is it ok for gerrit case?
3286 base_branch = args[0]
3287 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003288 if cl.GetBranch() is None:
3289 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3290
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003291 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003292 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003293 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003294
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003295 # Make sure authenticated to Rietveld before running expensive hooks. It is
3296 # a fast, best efforts check. Rietveld still can reject the authentication
3297 # during the actual upload.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003298 if not cl.IsGerrit() and auth_config.use_oauth2:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003299 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003300 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003301 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003302 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003303
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003304 # Apply watchlists on upload.
3305 change = cl.GetChange(base_branch, None)
3306 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3307 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003308 if not options.bypass_watchlists:
3309 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003310
ukai@chromium.orge8077812012-02-03 03:41:46 +00003311 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003312 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003313 # Set the reviewer list now so that presubmit checks can access it.
3314 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003315 change_description.update_reviewers(options.reviewers,
3316 options.tbr_owners,
3317 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003318 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003319 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003320 may_prompt=not options.force,
3321 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003322 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003323 if not hook_results.should_continue():
3324 return 1
3325 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003326 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003327
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003328 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003329 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003330 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003331 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003332 print ('The last upload made from this repository was patchset #%d but '
3333 'the most recent patchset on the server is #%d.'
3334 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003335 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3336 'from another machine or branch the patch you\'re uploading now '
3337 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003338 ask_for_data('About to upload; enter to confirm.')
3339
iannucci@chromium.org79540052012-10-19 23:15:26 +00003340 print_stats(options.similarity, options.find_copies, args)
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003341 if cl.IsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003342 if options.squash and options.no_squash:
3343 DieWithError('Can only use one of --squash or --no-squash')
3344
3345 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3346 not options.no_squash)
3347
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00003348 ret = GerritUpload(options, args, cl, change)
3349 else:
3350 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003351 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003352 git_set_branch_value('last-upload-hash',
3353 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003354 # Run post upload hooks, if specified.
3355 if settings.GetRunPostUploadHook():
3356 presubmit_support.DoPostUploadExecuter(
3357 change,
3358 cl,
3359 settings.GetRoot(),
3360 options.verbose,
3361 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003362
rmistry@google.com2dd99862015-06-22 12:22:18 +00003363 # Upload all dependencies if specified.
3364 if options.dependencies:
3365 print
3366 print '--dependencies has been specified.'
3367 print 'All dependent local branches will be re-uploaded.'
3368 print
3369 # Remove the dependencies flag from args so that we do not end up in a
3370 # loop.
3371 orig_args.remove('--dependencies')
3372 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003373 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003374
3375
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003376def IsSubmoduleMergeCommit(ref):
3377 # When submodules are added to the repo, we expect there to be a single
3378 # non-git-svn merge commit at remote HEAD with a signature comment.
3379 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003380 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003381 return RunGit(cmd) != ''
3382
3383
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003384def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003385 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003386
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003387 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3388 upstream and closes the issue automatically and atomically.
3389
3390 Otherwise (in case of Rietveld):
3391 Squashes branch into a single commit.
3392 Updates changelog with metadata (e.g. pointer to review).
3393 Pushes/dcommits the code upstream.
3394 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003395 """
3396 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3397 help='bypass upload presubmit hook')
3398 parser.add_option('-m', dest='message',
3399 help="override review description")
3400 parser.add_option('-f', action='store_true', dest='force',
3401 help="force yes to questions (don't prompt)")
3402 parser.add_option('-c', dest='contributor',
3403 help="external contributor for patch (appended to " +
3404 "description and used as author for git). Should be " +
3405 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003406 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003407 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003409 auth_config = auth.extract_auth_config_from_options(options)
3410
3411 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003412
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003413 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3414 if cl.IsGerrit():
3415 if options.message:
3416 # This could be implemented, but it requires sending a new patch to
3417 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3418 # Besides, Gerrit has the ability to change the commit message on submit
3419 # automatically, thus there is no need to support this option (so far?).
3420 parser.error('-m MESSAGE option is not supported for Gerrit.')
3421 if options.contributor:
3422 parser.error(
3423 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3424 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3425 'the contributor\'s "name <email>". If you can\'t upload such a '
3426 'commit for review, contact your repository admin and request'
3427 '"Forge-Author" permission.')
3428 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3429 options.verbose)
3430
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003431 current = cl.GetBranch()
3432 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3433 if not settings.GetIsGitSvn() and remote == '.':
3434 print
3435 print 'Attempting to push branch %r into another local branch!' % current
3436 print
3437 print 'Either reparent this branch on top of origin/master:'
3438 print ' git reparent-branch --root'
3439 print
3440 print 'OR run `git rebase-update` if you think the parent branch is already'
3441 print 'committed.'
3442 print
3443 print ' Current parent: %r' % upstream_branch
3444 return 1
3445
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003446 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003447 # Default to merging against our best guess of the upstream branch.
3448 args = [cl.GetUpstreamBranch()]
3449
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003450 if options.contributor:
3451 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3452 print "Please provide contibutor as 'First Last <email@example.com>'"
3453 return 1
3454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003455 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003456 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003457
sbc@chromium.org71437c02015-04-09 19:29:40 +00003458 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003459 return 1
3460
3461 # This rev-list syntax means "show all commits not in my branch that
3462 # are in base_branch".
3463 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3464 base_branch]).splitlines()
3465 if upstream_commits:
3466 print ('Base branch "%s" has %d commits '
3467 'not in this branch.' % (base_branch, len(upstream_commits)))
3468 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3469 return 1
3470
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003471 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003472 svn_head = None
3473 if cmd == 'dcommit' or base_has_submodules:
3474 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3475 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003476
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003477 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003478 # If the base_head is a submodule merge commit, the first parent of the
3479 # base_head should be a git-svn commit, which is what we're interested in.
3480 base_svn_head = base_branch
3481 if base_has_submodules:
3482 base_svn_head += '^1'
3483
3484 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003485 if extra_commits:
3486 print ('This branch has %d additional commits not upstreamed yet.'
3487 % len(extra_commits.splitlines()))
3488 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3489 'before attempting to %s.' % (base_branch, cmd))
3490 return 1
3491
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003492 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003493 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003494 author = None
3495 if options.contributor:
3496 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003497 hook_results = cl.RunHook(
3498 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003499 may_prompt=not options.force,
3500 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003501 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003502 if not hook_results.should_continue():
3503 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003504
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003505 # Check the tree status if the tree status URL is set.
3506 status = GetTreeStatus()
3507 if 'closed' == status:
3508 print('The tree is closed. Please wait for it to reopen. Use '
3509 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3510 return 1
3511 elif 'unknown' == status:
3512 print('Unable to determine tree status. Please verify manually and '
3513 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3514 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003515
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003516 change_desc = ChangeDescription(options.message)
3517 if not change_desc.description and cl.GetIssue():
3518 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003519
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003520 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003521 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003522 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003523 else:
3524 print 'No description set.'
3525 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3526 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003527
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003528 # Keep a separate copy for the commit message, because the commit message
3529 # contains the link to the Rietveld issue, while the Rietveld message contains
3530 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003531 # Keep a separate copy for the commit message.
3532 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003533 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003534
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003535 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003536 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003537 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003538 # after it. Add a period on a new line to circumvent this. Also add a space
3539 # before the period to make sure that Gitiles continues to correctly resolve
3540 # the URL.
3541 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003542 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003543 commit_desc.append_footer('Patch from %s.' % options.contributor)
3544
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003545 print('Description:')
3546 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003548 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003550 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003551
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003552 # We want to squash all this branch's commits into one commit with the proper
3553 # description. We do this by doing a "reset --soft" to the base branch (which
3554 # keeps the working copy the same), then dcommitting that. If origin/master
3555 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3556 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003557 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003558 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3559 # Delete the branches if they exist.
3560 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3561 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3562 result = RunGitWithCode(showref_cmd)
3563 if result[0] == 0:
3564 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003565
3566 # We might be in a directory that's present in this branch but not in the
3567 # trunk. Move up to the top of the tree so that git commands that expect a
3568 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003569 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003570 if rel_base_path:
3571 os.chdir(rel_base_path)
3572
3573 # Stuff our change into the merge branch.
3574 # We wrap in a try...finally block so if anything goes wrong,
3575 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003576 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003577 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003578 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003579 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003580 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003581 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003582 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003584 RunGit(
3585 [
3586 'commit', '--author', options.contributor,
3587 '-m', commit_desc.description,
3588 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003589 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003590 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003591 if base_has_submodules:
3592 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3593 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3594 RunGit(['checkout', CHERRY_PICK_BRANCH])
3595 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003596 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003597 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003598 mirror = settings.GetGitMirror(remote)
3599 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003600 pending_prefix = settings.GetPendingRefPrefix()
3601 if not pending_prefix or branch.startswith(pending_prefix):
3602 # If not using refs/pending/heads/* at all, or target ref is already set
3603 # to pending, then push to the target ref directly.
3604 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003605 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003606 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003607 else:
3608 # Cherry-pick the change on top of pending ref and then push it.
3609 assert branch.startswith('refs/'), branch
3610 assert pending_prefix[-1] == '/', pending_prefix
3611 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003612 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003613 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003614 if retcode == 0:
3615 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616 else:
3617 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003618 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003619 'svn', 'dcommit',
3620 '-C%s' % options.similarity,
3621 '--no-rebase', '--rmdir',
3622 ]
3623 if settings.GetForceHttpsCommitUrl():
3624 # Allow forcing https commit URLs for some projects that don't allow
3625 # committing to http URLs (like Google Code).
3626 remote_url = cl.GetGitSvnRemoteUrl()
3627 if urlparse.urlparse(remote_url).scheme == 'http':
3628 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003629 cmd_args.append('--commit-url=%s' % remote_url)
3630 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003631 if 'Committed r' in output:
3632 revision = re.match(
3633 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3634 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003635 finally:
3636 # And then swap back to the original branch and clean up.
3637 RunGit(['checkout', '-q', cl.GetBranch()])
3638 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003639 if base_has_submodules:
3640 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003641
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003642 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003643 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003644 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003645
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003646 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003647 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003648 try:
3649 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3650 # We set pushed_to_pending to False, since it made it all the way to the
3651 # real ref.
3652 pushed_to_pending = False
3653 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003654 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003657 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003658 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003659 if not to_pending:
3660 if viewvc_url and revision:
3661 change_desc.append_footer(
3662 'Committed: %s%s' % (viewvc_url, revision))
3663 elif revision:
3664 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 print ('Closing issue '
3666 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003667 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003668 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003669 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003670 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003671 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003672 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003673 if options.bypass_hooks:
3674 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3675 else:
3676 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003677 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003678 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003679
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003680 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003681 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3682 print 'The commit is in the pending queue (%s).' % pending_ref
3683 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003684 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003685 'footer.' % branch)
3686
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003687 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3688 if os.path.isfile(hook):
3689 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003690
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003691 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003692
3693
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003694def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3695 print
3696 print 'Waiting for commit to be landed on %s...' % real_ref
3697 print '(If you are impatient, you may Ctrl-C once without harm)'
3698 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3699 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003700 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003701
3702 loop = 0
3703 while True:
3704 sys.stdout.write('fetching (%d)... \r' % loop)
3705 sys.stdout.flush()
3706 loop += 1
3707
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003708 if mirror:
3709 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003710 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3711 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3712 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3713 for commit in commits.splitlines():
3714 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3715 print 'Found commit on %s' % real_ref
3716 return commit
3717
3718 current_rev = to_rev
3719
3720
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003721def PushToGitPending(remote, pending_ref, upstream_ref):
3722 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3723
3724 Returns:
3725 (retcode of last operation, output log of last operation).
3726 """
3727 assert pending_ref.startswith('refs/'), pending_ref
3728 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3729 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3730 code = 0
3731 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003732 max_attempts = 3
3733 attempts_left = max_attempts
3734 while attempts_left:
3735 if attempts_left != max_attempts:
3736 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3737 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003738
3739 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003740 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003741 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003742 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003743 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003744 print 'Fetch failed with exit code %d.' % code
3745 if out.strip():
3746 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003747 continue
3748
3749 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003750 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003751 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003752 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003753 if code:
3754 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003755 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3756 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003757 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3758 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003759 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003760 return code, out
3761
3762 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003763 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003764 code, out = RunGitWithCode(
3765 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3766 if code == 0:
3767 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003768 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003769 return code, out
3770
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003771 print 'Push failed with exit code %d.' % code
3772 if out.strip():
3773 print out.strip()
3774 if IsFatalPushFailure(out):
3775 print (
3776 'Fatal push error. Make sure your .netrc credentials and git '
3777 'user.email are correct and you have push access to the repo.')
3778 return code, out
3779
3780 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003781 return code, out
3782
3783
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003784def IsFatalPushFailure(push_stdout):
3785 """True if retrying push won't help."""
3786 return '(prohibited by Gerrit)' in push_stdout
3787
3788
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003789@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003790def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003791 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003792 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003793 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003794 # If it looks like previous commits were mirrored with git-svn.
3795 message = """This repository appears to be a git-svn mirror, but no
3796upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3797 else:
3798 message = """This doesn't appear to be an SVN repository.
3799If your project has a true, writeable git repository, you probably want to run
3800'git cl land' instead.
3801If your project has a git mirror of an upstream SVN master, you probably need
3802to run 'git svn init'.
3803
3804Using the wrong command might cause your commit to appear to succeed, and the
3805review to be closed, without actually landing upstream. If you choose to
3806proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003807 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003808 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003809 return SendUpstream(parser, args, 'dcommit')
3810
3811
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003812@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003813def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003814 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003815 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816 print('This appears to be an SVN repository.')
3817 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003818 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003819 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003820 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003821
3822
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003823@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003825 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003826 parser.add_option('-b', dest='newbranch',
3827 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003828 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003829 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003830 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3831 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003832 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003833 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003834 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003835 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003836 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003837 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003838
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003839
3840 group = optparse.OptionGroup(
3841 parser,
3842 'Options for continuing work on the current issue uploaded from a '
3843 'different clone (e.g. different machine). Must be used independently '
3844 'from the other options. No issue number should be specified, and the '
3845 'branch must have an issue number associated with it')
3846 group.add_option('--reapply', action='store_true', dest='reapply',
3847 help='Reset the branch and reapply the issue.\n'
3848 'CAUTION: This will undo any local changes in this '
3849 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003850
3851 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003852 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003853 parser.add_option_group(group)
3854
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003855 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003856 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003857 auth_config = auth.extract_auth_config_from_options(options)
3858
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003859 cl = Changelist(auth_config=auth_config)
3860
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003861 issue_arg = None
3862 if options.reapply :
3863 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003864 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003865
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003866 issue_arg = cl.GetIssue()
3867 upstream = cl.GetUpstreamBranch()
3868 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003869 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003870
3871 RunGit(['reset', '--hard', upstream])
3872 if options.pull:
3873 RunGit(['pull'])
3874 else:
3875 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003876 parser.error('Must specify issue number or url')
3877 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003878
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003879 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003880 parser.print_help()
3881 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003883 if cl.IsGerrit():
3884 if options.reject:
3885 parser.error('--reject is not supported with Gerrit codereview.')
3886 if options.nocommit:
3887 parser.error('--nocommit is not supported with Gerrit codereview.')
3888 if options.directory:
3889 parser.error('--directory is not supported with Gerrit codereview.')
3890
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003891 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003892 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003893 return 1
3894
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003895 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003896 if options.reapply:
3897 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003898 if options.force:
3899 RunGit(['branch', '-D', options.newbranch],
3900 stderr=subprocess2.PIPE, error_ok=True)
3901 RunGit(['checkout', '-b', options.newbranch,
3902 Changelist().GetUpstreamBranch()])
3903
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003904 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3905 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003906
3907
3908def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003909 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910 # Provide a wrapper for git svn rebase to help avoid accidental
3911 # git svn dcommit.
3912 # It's the only command that doesn't use parser at all since we just defer
3913 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003914
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003915 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003916
3917
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003918def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003919 """Fetches the tree status and returns either 'open', 'closed',
3920 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003921 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003922 if url:
3923 status = urllib2.urlopen(url).read().lower()
3924 if status.find('closed') != -1 or status == '0':
3925 return 'closed'
3926 elif status.find('open') != -1 or status == '1':
3927 return 'open'
3928 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003929 return 'unset'
3930
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003931
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932def GetTreeStatusReason():
3933 """Fetches the tree status from a json url and returns the message
3934 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003935 url = settings.GetTreeStatusUrl()
3936 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003937 connection = urllib2.urlopen(json_url)
3938 status = json.loads(connection.read())
3939 connection.close()
3940 return status['message']
3941
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003942
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003943def GetBuilderMaster(bot_list):
3944 """For a given builder, fetch the master from AE if available."""
3945 map_url = 'https://builders-map.appspot.com/'
3946 try:
3947 master_map = json.load(urllib2.urlopen(map_url))
3948 except urllib2.URLError as e:
3949 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3950 (map_url, e))
3951 except ValueError as e:
3952 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3953 if not master_map:
3954 return None, 'Failed to build master map.'
3955
3956 result_master = ''
3957 for bot in bot_list:
3958 builder = bot.split(':', 1)[0]
3959 master_list = master_map.get(builder, [])
3960 if not master_list:
3961 return None, ('No matching master for builder %s.' % builder)
3962 elif len(master_list) > 1:
3963 return None, ('The builder name %s exists in multiple masters %s.' %
3964 (builder, master_list))
3965 else:
3966 cur_master = master_list[0]
3967 if not result_master:
3968 result_master = cur_master
3969 elif result_master != cur_master:
3970 return None, 'The builders do not belong to the same master.'
3971 return result_master, None
3972
3973
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003975 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003976 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003977 status = GetTreeStatus()
3978 if 'unset' == status:
3979 print 'You must configure your tree status URL by running "git cl config".'
3980 return 2
3981
3982 print "The tree is %s" % status
3983 print
3984 print GetTreeStatusReason()
3985 if status != 'open':
3986 return 1
3987 return 0
3988
3989
maruel@chromium.org15192402012-09-06 12:38:29 +00003990def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003991 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003992 group = optparse.OptionGroup(parser, "Try job options")
3993 group.add_option(
3994 "-b", "--bot", action="append",
3995 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3996 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003997 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003998 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003999 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004000 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004001 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004002 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004003 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004004 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004005 "-r", "--revision",
4006 help="Revision to use for the try job; default: the "
4007 "revision will be determined by the try server; see "
4008 "its waterfall for more info")
4009 group.add_option(
4010 "-c", "--clobber", action="store_true", default=False,
4011 help="Force a clobber before building; e.g. don't do an "
4012 "incremental build")
4013 group.add_option(
4014 "--project",
4015 help="Override which project to use. Projects are defined "
4016 "server-side to define what default bot set to use")
4017 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004018 "-p", "--property", dest="properties", action="append", default=[],
4019 help="Specify generic properties in the form -p key1=value1 -p "
4020 "key2=value2 etc (buildbucket only). The value will be treated as "
4021 "json if decodable, or as string otherwise.")
4022 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004023 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004024 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004025 "--use-rietveld", action="store_true", default=False,
4026 help="Use Rietveld to trigger try jobs.")
4027 group.add_option(
4028 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4029 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004030 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004031 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004032 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004033 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004034
machenbach@chromium.org45453142015-09-15 08:45:22 +00004035 if options.use_rietveld and options.properties:
4036 parser.error('Properties can only be specified with buildbucket')
4037
4038 # Make sure that all properties are prop=value pairs.
4039 bad_params = [x for x in options.properties if '=' not in x]
4040 if bad_params:
4041 parser.error('Got properties with missing "=": %s' % bad_params)
4042
maruel@chromium.org15192402012-09-06 12:38:29 +00004043 if args:
4044 parser.error('Unknown arguments: %s' % args)
4045
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004046 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004047 if not cl.GetIssue():
4048 parser.error('Need to upload first')
4049
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004050 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004051 if props.get('closed'):
4052 parser.error('Cannot send tryjobs for a closed CL')
4053
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004054 if props.get('private'):
4055 parser.error('Cannot use trybots with private issue')
4056
maruel@chromium.org15192402012-09-06 12:38:29 +00004057 if not options.name:
4058 options.name = cl.GetBranch()
4059
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004060 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004061 options.master, err_msg = GetBuilderMaster(options.bot)
4062 if err_msg:
4063 parser.error('Tryserver master cannot be found because: %s\n'
4064 'Please manually specify the tryserver master'
4065 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004066
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004067 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004068 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004069 if not options.bot:
4070 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004071
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004072 # Get try masters from PRESUBMIT.py files.
4073 masters = presubmit_support.DoGetTryMasters(
4074 change,
4075 change.LocalPaths(),
4076 settings.GetRoot(),
4077 None,
4078 None,
4079 options.verbose,
4080 sys.stdout)
4081 if masters:
4082 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004083
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004084 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4085 options.bot = presubmit_support.DoGetTrySlaves(
4086 change,
4087 change.LocalPaths(),
4088 settings.GetRoot(),
4089 None,
4090 None,
4091 options.verbose,
4092 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004093
4094 if not options.bot:
4095 # Get try masters from cq.cfg if any.
4096 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4097 # location.
4098 cq_cfg = os.path.join(change.RepositoryRoot(),
4099 'infra', 'config', 'cq.cfg')
4100 if os.path.exists(cq_cfg):
4101 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004102 cq_masters = commit_queue.get_master_builder_map(
4103 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004104 for master, builders in cq_masters.iteritems():
4105 for builder in builders:
4106 # Skip presubmit builders, because these will fail without LGTM.
4107 if 'presubmit' not in builder.lower():
4108 masters.setdefault(master, {})[builder] = ['defaulttests']
4109 if masters:
4110 return masters
4111
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004112 if not options.bot:
4113 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004114
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004115 builders_and_tests = {}
4116 # TODO(machenbach): The old style command-line options don't support
4117 # multiple try masters yet.
4118 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4119 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4120
4121 for bot in old_style:
4122 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004123 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004124 elif ',' in bot:
4125 parser.error('Specify one bot per --bot flag')
4126 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004127 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004128
4129 for bot, tests in new_style:
4130 builders_and_tests.setdefault(bot, []).extend(tests)
4131
4132 # Return a master map with one master to be backwards compatible. The
4133 # master name defaults to an empty string, which will cause the master
4134 # not to be set on rietveld (deprecated).
4135 return {options.master: builders_and_tests}
4136
4137 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004138
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004139 for builders in masters.itervalues():
4140 if any('triggered' in b for b in builders):
4141 print >> sys.stderr, (
4142 'ERROR You are trying to send a job to a triggered bot. This type of'
4143 ' bot requires an\ninitial job from a parent (usually a builder). '
4144 'Instead send your job to the parent.\n'
4145 'Bot list: %s' % builders)
4146 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004147
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004148 patchset = cl.GetMostRecentPatchset()
4149 if patchset and patchset != cl.GetPatchset():
4150 print(
4151 '\nWARNING Mismatch between local config and server. Did a previous '
4152 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4153 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004154 if options.luci:
4155 trigger_luci_job(cl, masters, options)
4156 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004157 try:
4158 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4159 except BuildbucketResponseException as ex:
4160 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004161 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004162 except Exception as e:
4163 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4164 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4165 e, stacktrace)
4166 return 1
4167 else:
4168 try:
4169 cl.RpcServer().trigger_distributed_try_jobs(
4170 cl.GetIssue(), patchset, options.name, options.clobber,
4171 options.revision, masters)
4172 except urllib2.HTTPError as e:
4173 if e.code == 404:
4174 print('404 from rietveld; '
4175 'did you mean to use "git try" instead of "git cl try"?')
4176 return 1
4177 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004178
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004179 for (master, builders) in sorted(masters.iteritems()):
4180 if master:
4181 print 'Master: %s' % master
4182 length = max(len(builder) for builder in builders)
4183 for builder in sorted(builders):
4184 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004185 return 0
4186
4187
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004188def CMDtry_results(parser, args):
4189 group = optparse.OptionGroup(parser, "Try job results options")
4190 group.add_option(
4191 "-p", "--patchset", type=int, help="patchset number if not current.")
4192 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004193 "--print-master", action='store_true', help="print master name as well.")
4194 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004195 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004196 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004197 group.add_option(
4198 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4199 help="Host of buildbucket. The default host is %default.")
4200 parser.add_option_group(group)
4201 auth.add_auth_options(parser)
4202 options, args = parser.parse_args(args)
4203 if args:
4204 parser.error('Unrecognized args: %s' % ' '.join(args))
4205
4206 auth_config = auth.extract_auth_config_from_options(options)
4207 cl = Changelist(auth_config=auth_config)
4208 if not cl.GetIssue():
4209 parser.error('Need to upload first')
4210
4211 if not options.patchset:
4212 options.patchset = cl.GetMostRecentPatchset()
4213 if options.patchset and options.patchset != cl.GetPatchset():
4214 print(
4215 '\nWARNING Mismatch between local config and server. Did a previous '
4216 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4217 'Continuing using\npatchset %s.\n' % options.patchset)
4218 try:
4219 jobs = fetch_try_jobs(auth_config, cl, options)
4220 except BuildbucketResponseException as ex:
4221 print 'Buildbucket error: %s' % ex
4222 return 1
4223 except Exception as e:
4224 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4225 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4226 e, stacktrace)
4227 return 1
4228 print_tryjobs(options, jobs)
4229 return 0
4230
4231
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004232@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004233def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004234 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004235 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004236 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004237 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004238
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004239 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004240 if args:
4241 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004242 branch = cl.GetBranch()
4243 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004244 cl = Changelist()
4245 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004246
4247 # Clear configured merge-base, if there is one.
4248 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004249 else:
4250 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004251 return 0
4252
4253
thestig@chromium.org00858c82013-12-02 23:08:03 +00004254def CMDweb(parser, args):
4255 """Opens the current CL in the web browser."""
4256 _, args = parser.parse_args(args)
4257 if args:
4258 parser.error('Unrecognized args: %s' % ' '.join(args))
4259
4260 issue_url = Changelist().GetIssueURL()
4261 if not issue_url:
4262 print >> sys.stderr, 'ERROR No issue to open'
4263 return 1
4264
4265 webbrowser.open(issue_url)
4266 return 0
4267
4268
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004269def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004270 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004271 auth.add_auth_options(parser)
4272 options, args = parser.parse_args(args)
4273 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004274 if args:
4275 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004276 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004277 props = cl.GetIssueProperties()
4278 if props.get('private'):
4279 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004280 cl.SetFlag('commit', '1')
4281 return 0
4282
4283
groby@chromium.org411034a2013-02-26 15:12:01 +00004284def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004285 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004286 auth.add_auth_options(parser)
4287 options, args = parser.parse_args(args)
4288 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004289 if args:
4290 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004291 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004292 # Ensure there actually is an issue to close.
4293 cl.GetDescription()
4294 cl.CloseIssue()
4295 return 0
4296
4297
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004298def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004299 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004300 auth.add_auth_options(parser)
4301 options, args = parser.parse_args(args)
4302 auth_config = auth.extract_auth_config_from_options(options)
4303 if args:
4304 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004305
4306 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004307 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004308 # Staged changes would be committed along with the patch from last
4309 # upload, hence counted toward the "last upload" side in the final
4310 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004311 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004312 return 1
4313
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004314 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004315 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004316 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004317 if not issue:
4318 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004319 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004320 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004321
4322 # Create a new branch based on the merge-base
4323 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004324 # Clear cached branch in cl object, to avoid overwriting original CL branch
4325 # properties.
4326 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004327 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004328 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004329 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004330 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004331 return rtn
4332
wychen@chromium.org06928532015-02-03 02:11:29 +00004333 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004334 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004335 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004336 finally:
4337 RunGit(['checkout', '-q', branch])
4338 RunGit(['branch', '-D', TMP_BRANCH])
4339
4340 return 0
4341
4342
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004343def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004344 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004345 parser.add_option(
4346 '--no-color',
4347 action='store_true',
4348 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004349 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004350 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004351 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004352
4353 author = RunGit(['config', 'user.email']).strip() or None
4354
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004355 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004356
4357 if args:
4358 if len(args) > 1:
4359 parser.error('Unknown args')
4360 base_branch = args[0]
4361 else:
4362 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004363 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004364
4365 change = cl.GetChange(base_branch, None)
4366 return owners_finder.OwnersFinder(
4367 [f.LocalPath() for f in
4368 cl.GetChange(base_branch, None).AffectedFiles()],
4369 change.RepositoryRoot(), author,
4370 fopen=file, os_path=os.path, glob=glob.glob,
4371 disable_color=options.no_color).run()
4372
4373
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004374def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004375 """Generates a diff command."""
4376 # Generate diff for the current branch's changes.
4377 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4378 upstream_commit, '--' ]
4379
4380 if args:
4381 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004382 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004383 diff_cmd.append(arg)
4384 else:
4385 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004386
4387 return diff_cmd
4388
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004389def MatchingFileType(file_name, extensions):
4390 """Returns true if the file name ends with one of the given extensions."""
4391 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004392
enne@chromium.org555cfe42014-01-29 18:21:39 +00004393@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004394def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004395 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004396 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004397 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004398 parser.add_option('--full', action='store_true',
4399 help='Reformat the full content of all touched files')
4400 parser.add_option('--dry-run', action='store_true',
4401 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004402 parser.add_option('--python', action='store_true',
4403 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004404 parser.add_option('--diff', action='store_true',
4405 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004406 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004407
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004408 # git diff generates paths against the root of the repository. Change
4409 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004410 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004411 if rel_base_path:
4412 os.chdir(rel_base_path)
4413
digit@chromium.org29e47272013-05-17 17:01:46 +00004414 # Grab the merge-base commit, i.e. the upstream commit of the current
4415 # branch when it was created or the last time it was rebased. This is
4416 # to cover the case where the user may have called "git fetch origin",
4417 # moving the origin branch to a newer commit, but hasn't rebased yet.
4418 upstream_commit = None
4419 cl = Changelist()
4420 upstream_branch = cl.GetUpstreamBranch()
4421 if upstream_branch:
4422 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4423 upstream_commit = upstream_commit.strip()
4424
4425 if not upstream_commit:
4426 DieWithError('Could not find base commit for this branch. '
4427 'Are you in detached state?')
4428
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004429 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4430 diff_output = RunGit(changed_files_cmd)
4431 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004432 # Filter out files deleted by this CL
4433 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004434
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004435 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4436 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4437 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004438 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004439
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004440 top_dir = os.path.normpath(
4441 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4442
4443 # Locate the clang-format binary in the checkout
4444 try:
4445 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4446 except clang_format.NotFoundError, e:
4447 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004448
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004449 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4450 # formatted. This is used to block during the presubmit.
4451 return_value = 0
4452
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004453 if clang_diff_files:
4454 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004455 cmd = [clang_format_tool]
4456 if not opts.dry_run and not opts.diff:
4457 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004458 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004459 if opts.diff:
4460 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004461 else:
4462 env = os.environ.copy()
4463 env['PATH'] = str(os.path.dirname(clang_format_tool))
4464 try:
4465 script = clang_format.FindClangFormatScriptInChromiumTree(
4466 'clang-format-diff.py')
4467 except clang_format.NotFoundError, e:
4468 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004469
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004470 cmd = [sys.executable, script, '-p0']
4471 if not opts.dry_run and not opts.diff:
4472 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004473
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004474 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4475 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004476
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004477 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4478 if opts.diff:
4479 sys.stdout.write(stdout)
4480 if opts.dry_run and len(stdout) > 0:
4481 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004482
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004483 # Similar code to above, but using yapf on .py files rather than clang-format
4484 # on C/C++ files
4485 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004486 yapf_tool = gclient_utils.FindExecutable('yapf')
4487 if yapf_tool is None:
4488 DieWithError('yapf not found in PATH')
4489
4490 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004491 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004492 cmd = [yapf_tool]
4493 if not opts.dry_run and not opts.diff:
4494 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004495 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004496 if opts.diff:
4497 sys.stdout.write(stdout)
4498 else:
4499 # TODO(sbc): yapf --lines mode still has some issues.
4500 # https://github.com/google/yapf/issues/154
4501 DieWithError('--python currently only works with --full')
4502
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004503 # Dart's formatter does not have the nice property of only operating on
4504 # modified chunks, so hard code full.
4505 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004506 try:
4507 command = [dart_format.FindDartFmtToolInChromiumTree()]
4508 if not opts.dry_run and not opts.diff:
4509 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004510 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004511
ppi@chromium.org6593d932016-03-03 15:41:15 +00004512 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004513 if opts.dry_run and stdout:
4514 return_value = 2
4515 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004516 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4517 'found in this checkout. Files in other languages are still ' +
4518 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004519
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004520 # Format GN build files. Always run on full build files for canonical form.
4521 if gn_diff_files:
4522 cmd = ['gn', 'format']
4523 if not opts.dry_run and not opts.diff:
4524 cmd.append('--in-place')
4525 for gn_diff_file in gn_diff_files:
4526 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4527 if opts.diff:
4528 sys.stdout.write(stdout)
4529
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004530 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004531
4532
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004533@subcommand.usage('<codereview url or issue id>')
4534def CMDcheckout(parser, args):
4535 """Checks out a branch associated with a given Rietveld issue."""
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004536 # TODO(tandrii): consider adding this for Gerrit?
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004537 _, args = parser.parse_args(args)
4538
4539 if len(args) != 1:
4540 parser.print_help()
4541 return 1
4542
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004543 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004544 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004545 parser.print_help()
4546 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004547 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004548
4549 key_and_issues = [x.split() for x in RunGit(
4550 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4551 .splitlines()]
4552 branches = []
4553 for key, issue in key_and_issues:
4554 if issue == target_issue:
4555 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4556
4557 if len(branches) == 0:
4558 print 'No branch found for issue %s.' % target_issue
4559 return 1
4560 if len(branches) == 1:
4561 RunGit(['checkout', branches[0]])
4562 else:
4563 print 'Multiple branches match issue %s:' % target_issue
4564 for i in range(len(branches)):
4565 print '%d: %s' % (i, branches[i])
4566 which = raw_input('Choose by index: ')
4567 try:
4568 RunGit(['checkout', branches[int(which)]])
4569 except (IndexError, ValueError):
4570 print 'Invalid selection, not checking out any branch.'
4571 return 1
4572
4573 return 0
4574
4575
maruel@chromium.org29404b52014-09-08 22:58:00 +00004576def CMDlol(parser, args):
4577 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004578 print zlib.decompress(base64.b64decode(
4579 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4580 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4581 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4582 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004583 return 0
4584
4585
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004586class OptionParser(optparse.OptionParser):
4587 """Creates the option parse and add --verbose support."""
4588 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004589 optparse.OptionParser.__init__(
4590 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004591 self.add_option(
4592 '-v', '--verbose', action='count', default=0,
4593 help='Use 2 times for more debugging info')
4594
4595 def parse_args(self, args=None, values=None):
4596 options, args = optparse.OptionParser.parse_args(self, args, values)
4597 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4598 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4599 return options, args
4600
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004601
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004602def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004603 if sys.hexversion < 0x02060000:
4604 print >> sys.stderr, (
4605 '\nYour python version %s is unsupported, please upgrade.\n' %
4606 sys.version.split(' ', 1)[0])
4607 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004608
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004609 # Reload settings.
4610 global settings
4611 settings = Settings()
4612
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004613 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004614 dispatcher = subcommand.CommandDispatcher(__name__)
4615 try:
4616 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004617 except auth.AuthenticationError as e:
4618 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004619 except urllib2.HTTPError, e:
4620 if e.code != 500:
4621 raise
4622 DieWithError(
4623 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4624 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004625 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004626
4627
4628if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004629 # These affect sys.stdout so do it outside of main() to simplify mocks in
4630 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004631 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004632 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004633 try:
4634 sys.exit(main(sys.argv[1:]))
4635 except KeyboardInterrupt:
4636 sys.stderr.write('interrupted\n')
4637 sys.exit(1)