blob: 98d9c673784df3775b1bb930d632b85f01db27a2 [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
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000995 @staticmethod
996 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000997 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 e.g. 'origin', 'refs/heads/master'
999 """
1000 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1002 error_ok=True).strip()
1003 if upstream_branch:
1004 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1005 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001006 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1007 error_ok=True).strip()
1008 if upstream_branch:
1009 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001011 # Fall back on trying a git-svn upstream branch.
1012 if settings.GetIsGitSvn():
1013 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Else, try to guess the origin remote.
1016 remote_branches = RunGit(['branch', '-r']).split()
1017 if 'origin/master' in remote_branches:
1018 # Fall back on origin/master if it exits.
1019 remote = 'origin'
1020 upstream_branch = 'refs/heads/master'
1021 elif 'origin/trunk' in remote_branches:
1022 # Fall back on origin/trunk if it exists. Generally a shared
1023 # git-svn clone
1024 remote = 'origin'
1025 upstream_branch = 'refs/heads/trunk'
1026 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027 DieWithError(
1028 'Unable to determine default branch to diff against.\n'
1029 'Either pass complete "git diff"-style arguments, like\n'
1030 ' git cl upload origin/master\n'
1031 'or verify this branch is set up to track another \n'
1032 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033
1034 return remote, upstream_branch
1035
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001036 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001037 upstream_branch = self.GetUpstreamBranch()
1038 if not BranchExists(upstream_branch):
1039 DieWithError('The upstream for the current branch (%s) does not exist '
1040 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001041 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001042 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 def GetUpstreamBranch(self):
1045 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001046 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001048 upstream_branch = upstream_branch.replace('refs/heads/',
1049 'refs/remotes/%s/' % remote)
1050 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1051 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052 self.upstream_branch = upstream_branch
1053 return self.upstream_branch
1054
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001056 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001057 remote, branch = None, self.GetBranch()
1058 seen_branches = set()
1059 while branch not in seen_branches:
1060 seen_branches.add(branch)
1061 remote, branch = self.FetchUpstreamTuple(branch)
1062 branch = ShortBranchName(branch)
1063 if remote != '.' or branch.startswith('refs/remotes'):
1064 break
1065 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001066 remotes = RunGit(['remote'], error_ok=True).split()
1067 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001068 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001069 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001070 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001071 logging.warning('Could not determine which remote this change is '
1072 'associated with, so defaulting to "%s". This may '
1073 'not be what you want. You may prevent this message '
1074 'by running "git svn info" as documented here: %s',
1075 self._remote,
1076 GIT_INSTRUCTIONS_URL)
1077 else:
1078 logging.warn('Could not determine which remote this change is '
1079 'associated with. You may prevent this message by '
1080 'running "git svn info" as documented here: %s',
1081 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001082 branch = 'HEAD'
1083 if branch.startswith('refs/remotes'):
1084 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001085 elif branch.startswith('refs/branch-heads/'):
1086 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001087 else:
1088 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001089 return self._remote
1090
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 def GitSanityChecks(self, upstream_git_obj):
1092 """Checks git repo status and ensures diff is from local commits."""
1093
sbc@chromium.org79706062015-01-14 21:18:12 +00001094 if upstream_git_obj is None:
1095 if self.GetBranch() is None:
1096 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001097 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 else:
1099 print >> sys.stderr, (
1100 'ERROR: no upstream branch')
1101 return False
1102
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 # Verify the commit we're diffing against is in our current branch.
1104 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1105 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1106 if upstream_sha != common_ancestor:
1107 print >> sys.stderr, (
1108 'ERROR: %s is not in the current branch. You may need to rebase '
1109 'your tracking branch' % upstream_sha)
1110 return False
1111
1112 # List the commits inside the diff, and verify they are all local.
1113 commits_in_diff = RunGit(
1114 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1115 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1116 remote_branch = remote_branch.strip()
1117 if code != 0:
1118 _, remote_branch = self.GetRemoteBranch()
1119
1120 commits_in_remote = RunGit(
1121 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1122
1123 common_commits = set(commits_in_diff) & set(commits_in_remote)
1124 if common_commits:
1125 print >> sys.stderr, (
1126 'ERROR: Your diff contains %d commits already in %s.\n'
1127 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1128 'the diff. If you are using a custom git flow, you can override'
1129 ' the reference used for this check with "git config '
1130 'gitcl.remotebranch <git-ref>".' % (
1131 len(common_commits), remote_branch, upstream_git_obj))
1132 return False
1133 return True
1134
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001135 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001136 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001137
1138 Returns None if it is not set.
1139 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1141 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001142
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001143 def GetGitSvnRemoteUrl(self):
1144 """Return the configured git-svn remote URL parsed from git svn info.
1145
1146 Returns None if it is not set.
1147 """
1148 # URL is dependent on the current directory.
1149 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1150 if data:
1151 keys = dict(line.split(': ', 1) for line in data.splitlines()
1152 if ': ' in line)
1153 return keys.get('URL', None)
1154 return None
1155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 def GetRemoteUrl(self):
1157 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1158
1159 Returns None if there is no remote.
1160 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001161 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001162 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1163
1164 # If URL is pointing to a local directory, it is probably a git cache.
1165 if os.path.isdir(url):
1166 url = RunGit(['config', 'remote.%s.url' % remote],
1167 error_ok=True,
1168 cwd=url).strip()
1169 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001171 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001172 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001173 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174 issue = RunGit(['config',
1175 self._codereview_impl.IssueSetting(self.GetBranch())],
1176 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001177 self.issue = int(issue) or None if issue else None
1178 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179 return self.issue
1180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 def GetIssueURL(self):
1182 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001183 issue = self.GetIssue()
1184 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001185 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001186 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
1188 def GetDescription(self, pretty=False):
1189 if not self.has_description:
1190 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001191 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 self.has_description = True
1193 if pretty:
1194 wrapper = textwrap.TextWrapper()
1195 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1196 return wrapper.fill(self.description)
1197 return self.description
1198
1199 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001200 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001201 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001204 self.patchset = int(patchset) or None if patchset else None
1205 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 return self.patchset
1207
1208 def SetPatchset(self, patchset):
1209 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001210 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001212 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001213 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001215 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001216 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001219 def SetIssue(self, issue=None):
1220 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001221 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1222 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 RunGit(['config', issue_setting, str(issue)])
1226 codereview_server = self._codereview_impl.GetCodereviewServer()
1227 if codereview_server:
1228 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001230 current_issue = self.GetIssue()
1231 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233 self.issue = None
1234 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001236 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001237 if not self.GitSanityChecks(upstream_branch):
1238 DieWithError('\nGit sanity check failure')
1239
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001240 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001241 if not root:
1242 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001243 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001244
1245 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001246 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001248 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001249 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001250 except subprocess2.CalledProcessError:
1251 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001252 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001253 'This branch probably doesn\'t exist anymore. To reset the\n'
1254 'tracking branch, please run\n'
1255 ' git branch --set-upstream %s trunk\n'
1256 'replacing trunk with origin/master or the relevant branch') %
1257 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001258
maruel@chromium.org52424302012-08-29 15:14:30 +00001259 issue = self.GetIssue()
1260 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001261 if issue:
1262 description = self.GetDescription()
1263 else:
1264 # If the change was never uploaded, use the log messages of all commits
1265 # up to the branch point, as git cl upload will prefill the description
1266 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001267 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1268 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001269
1270 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001271 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001272 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001273 name,
1274 description,
1275 absroot,
1276 files,
1277 issue,
1278 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001279 author,
1280 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001281
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001282 def UpdateDescription(self, description):
1283 self.description = description
1284 return self._codereview_impl.UpdateDescriptionRemote(description)
1285
1286 def RunHook(self, committing, may_prompt, verbose, change):
1287 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1288 try:
1289 return presubmit_support.DoPresubmitChecks(change, committing,
1290 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1291 default_presubmit=None, may_prompt=may_prompt,
1292 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1293 except presubmit_support.PresubmitFailure, e:
1294 DieWithError(
1295 ('%s\nMaybe your depot_tools is out of date?\n'
1296 'If all fails, contact maruel@') % e)
1297
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001298 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1299 """Fetches and applies the issue patch from codereview to local branch."""
1300 if issue_arg.isdigit():
1301 parsed_issue_arg = _RietveldParsedIssueNumberArgument(int(issue_arg))
1302 else:
1303 # Assume url.
1304 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1305 urlparse.urlparse(issue_arg))
1306 if not parsed_issue_arg or not parsed_issue_arg.valid:
1307 DieWithError('Failed to parse issue argument "%s". '
1308 'Must be an issue number or a valid URL.' % issue_arg)
1309 return self._codereview_impl.CMDPatchWithParsedIssue(
1310 parsed_issue_arg, reject, nocommit, directory)
1311
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001312 # Forward methods to codereview specific implementation.
1313
1314 def CloseIssue(self):
1315 return self._codereview_impl.CloseIssue()
1316
1317 def GetStatus(self):
1318 return self._codereview_impl.GetStatus()
1319
1320 def GetCodereviewServer(self):
1321 return self._codereview_impl.GetCodereviewServer()
1322
1323 def GetApprovingReviewers(self):
1324 return self._codereview_impl.GetApprovingReviewers()
1325
1326 def GetMostRecentPatchset(self):
1327 return self._codereview_impl.GetMostRecentPatchset()
1328
1329 def __getattr__(self, attr):
1330 # This is because lots of untested code accesses Rietveld-specific stuff
1331 # directly, and it's hard to fix for sure. So, just let it work, and fix
1332 # on a cases by case basis.
1333 return getattr(self._codereview_impl, attr)
1334
1335
1336class _ChangelistCodereviewBase(object):
1337 """Abstract base class encapsulating codereview specifics of a changelist."""
1338 def __init__(self, changelist):
1339 self._changelist = changelist # instance of Changelist
1340
1341 def __getattr__(self, attr):
1342 # Forward methods to changelist.
1343 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1344 # _RietveldChangelistImpl to avoid this hack?
1345 return getattr(self._changelist, attr)
1346
1347 def GetStatus(self):
1348 """Apply a rough heuristic to give a simple summary of an issue's review
1349 or CQ status, assuming adherence to a common workflow.
1350
1351 Returns None if no issue for this branch, or specific string keywords.
1352 """
1353 raise NotImplementedError()
1354
1355 def GetCodereviewServer(self):
1356 """Returns server URL without end slash, like "https://codereview.com"."""
1357 raise NotImplementedError()
1358
1359 def FetchDescription(self):
1360 """Fetches and returns description from the codereview server."""
1361 raise NotImplementedError()
1362
1363 def GetCodereviewServerSetting(self):
1364 """Returns git config setting for the codereview server."""
1365 raise NotImplementedError()
1366
1367 @staticmethod
1368 def IssueSetting(branch):
1369 """Returns name of git config setting which stores issue number for a given
1370 branch."""
1371 raise NotImplementedError()
1372
1373 def PatchsetSetting(self):
1374 """Returns name of git config setting which stores issue number."""
1375 raise NotImplementedError()
1376
1377 def GetRieveldObjForPresubmit(self):
1378 # This is an unfortunate Rietveld-embeddedness in presubmit.
1379 # For non-Rietveld codereviews, this probably should return a dummy object.
1380 raise NotImplementedError()
1381
1382 def UpdateDescriptionRemote(self, description):
1383 """Update the description on codereview site."""
1384 raise NotImplementedError()
1385
1386 def CloseIssue(self):
1387 """Closes the issue."""
1388 raise NotImplementedError()
1389
1390 def GetApprovingReviewers(self):
1391 """Returns a list of reviewers approving the change.
1392
1393 Note: not necessarily committers.
1394 """
1395 raise NotImplementedError()
1396
1397 def GetMostRecentPatchset(self):
1398 """Returns the most recent patchset number from the codereview site."""
1399 raise NotImplementedError()
1400
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001401 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1402 directory):
1403 """Fetches and applies the issue.
1404
1405 Arguments:
1406 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1407 reject: if True, reject the failed patch instead of switching to 3-way
1408 merge. Rietveld only.
1409 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1410 only.
1411 directory: switch to directory before applying the patch. Rietveld only.
1412 """
1413 raise NotImplementedError()
1414
1415 @staticmethod
1416 def ParseIssueURL(parsed_url):
1417 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1418 failed."""
1419 raise NotImplementedError()
1420
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421
1422class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1423 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1424 super(_RietveldChangelistImpl, self).__init__(changelist)
1425 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1426 settings.GetDefaultServerUrl()
1427
1428 self._rietveld_server = rietveld_server
1429 self._auth_config = auth_config
1430 self._props = None
1431 self._rpc_server = None
1432
1433 def GetAuthConfig(self):
1434 return self._auth_config
1435
1436 def GetCodereviewServer(self):
1437 if not self._rietveld_server:
1438 # If we're on a branch then get the server potentially associated
1439 # with that branch.
1440 if self.GetIssue():
1441 rietveld_server_setting = self.GetCodereviewServerSetting()
1442 if rietveld_server_setting:
1443 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1444 ['config', rietveld_server_setting], error_ok=True).strip())
1445 if not self._rietveld_server:
1446 self._rietveld_server = settings.GetDefaultServerUrl()
1447 return self._rietveld_server
1448
1449 def FetchDescription(self):
1450 issue = self.GetIssue()
1451 assert issue
1452 try:
1453 return self.RpcServer().get_description(issue).strip()
1454 except urllib2.HTTPError as e:
1455 if e.code == 404:
1456 DieWithError(
1457 ('\nWhile fetching the description for issue %d, received a '
1458 '404 (not found)\n'
1459 'error. It is likely that you deleted this '
1460 'issue on the server. If this is the\n'
1461 'case, please run\n\n'
1462 ' git cl issue 0\n\n'
1463 'to clear the association with the deleted issue. Then run '
1464 'this command again.') % issue)
1465 else:
1466 DieWithError(
1467 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1468 except urllib2.URLError as e:
1469 print >> sys.stderr, (
1470 'Warning: Failed to retrieve CL description due to network '
1471 'failure.')
1472 return ''
1473
1474 def GetMostRecentPatchset(self):
1475 return self.GetIssueProperties()['patchsets'][-1]
1476
1477 def GetPatchSetDiff(self, issue, patchset):
1478 return self.RpcServer().get(
1479 '/download/issue%s_%s.diff' % (issue, patchset))
1480
1481 def GetIssueProperties(self):
1482 if self._props is None:
1483 issue = self.GetIssue()
1484 if not issue:
1485 self._props = {}
1486 else:
1487 self._props = self.RpcServer().get_issue_properties(issue, True)
1488 return self._props
1489
1490 def GetApprovingReviewers(self):
1491 return get_approving_reviewers(self.GetIssueProperties())
1492
1493 def AddComment(self, message):
1494 return self.RpcServer().add_comment(self.GetIssue(), message)
1495
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001496 def GetStatus(self):
1497 """Apply a rough heuristic to give a simple summary of an issue's review
1498 or CQ status, assuming adherence to a common workflow.
1499
1500 Returns None if no issue for this branch, or one of the following keywords:
1501 * 'error' - error from review tool (including deleted issues)
1502 * 'unsent' - not sent for review
1503 * 'waiting' - waiting for review
1504 * 'reply' - waiting for owner to reply to review
1505 * 'lgtm' - LGTM from at least one approved reviewer
1506 * 'commit' - in the commit queue
1507 * 'closed' - closed
1508 """
1509 if not self.GetIssue():
1510 return None
1511
1512 try:
1513 props = self.GetIssueProperties()
1514 except urllib2.HTTPError:
1515 return 'error'
1516
1517 if props.get('closed'):
1518 # Issue is closed.
1519 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001520 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001521 # Issue is in the commit queue.
1522 return 'commit'
1523
1524 try:
1525 reviewers = self.GetApprovingReviewers()
1526 except urllib2.HTTPError:
1527 return 'error'
1528
1529 if reviewers:
1530 # Was LGTM'ed.
1531 return 'lgtm'
1532
1533 messages = props.get('messages') or []
1534
1535 if not messages:
1536 # No message was sent.
1537 return 'unsent'
1538 if messages[-1]['sender'] != props.get('owner_email'):
1539 # Non-LGTM reply from non-owner
1540 return 'reply'
1541 return 'waiting'
1542
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001543 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001544 return self.RpcServer().update_description(
1545 self.GetIssue(), self.description)
1546
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001548 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001549
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001550 def SetFlag(self, flag, value):
1551 """Patchset must match."""
1552 if not self.GetPatchset():
1553 DieWithError('The patchset needs to match. Send another patchset.')
1554 try:
1555 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001556 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001557 except urllib2.HTTPError, e:
1558 if e.code == 404:
1559 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1560 if e.code == 403:
1561 DieWithError(
1562 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1563 'match?') % (self.GetIssue(), self.GetPatchset()))
1564 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001566 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567 """Returns an upload.RpcServer() to access this review's rietveld instance.
1568 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001569 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001570 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001571 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001572 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001573 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001574
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575 @staticmethod
1576 def IssueSetting(branch):
1577 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 """Return the git setting that stores this change's most recent patchset."""
1581 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001585 branch = self.GetBranch()
1586 if branch:
1587 return 'branch.%s.rietveldserver' % branch
1588 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590 def GetRieveldObjForPresubmit(self):
1591 return self.RpcServer()
1592
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001593 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1594 directory):
1595 # TODO(maruel): Use apply_issue.py
1596
1597 # PatchIssue should never be called with a dirty tree. It is up to the
1598 # caller to check this, but just in case we assert here since the
1599 # consequences of the caller not checking this could be dire.
1600 assert(not git_common.is_dirty_git_tree('apply'))
1601 assert(parsed_issue_arg.valid)
1602 self._changelist.issue = parsed_issue_arg.issue
1603 if parsed_issue_arg.hostname:
1604 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1605
1606 if parsed_issue_arg.patch_url:
1607 assert parsed_issue_arg.patchset
1608 patchset = parsed_issue_arg.patchset
1609 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1610 else:
1611 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1612 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1613
1614 # Switch up to the top-level directory, if necessary, in preparation for
1615 # applying the patch.
1616 top = settings.GetRelativeRoot()
1617 if top:
1618 os.chdir(top)
1619
1620 # Git patches have a/ at the beginning of source paths. We strip that out
1621 # with a sed script rather than the -p flag to patch so we can feed either
1622 # Git or svn-style patches into the same apply command.
1623 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1624 try:
1625 patch_data = subprocess2.check_output(
1626 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1627 except subprocess2.CalledProcessError:
1628 DieWithError('Git patch mungling failed.')
1629 logging.info(patch_data)
1630
1631 # We use "git apply" to apply the patch instead of "patch" so that we can
1632 # pick up file adds.
1633 # The --index flag means: also insert into the index (so we catch adds).
1634 cmd = ['git', 'apply', '--index', '-p0']
1635 if directory:
1636 cmd.extend(('--directory', directory))
1637 if reject:
1638 cmd.append('--reject')
1639 elif IsGitVersionAtLeast('1.7.12'):
1640 cmd.append('--3way')
1641 try:
1642 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1643 stdin=patch_data, stdout=subprocess2.VOID)
1644 except subprocess2.CalledProcessError:
1645 print 'Failed to apply the patch'
1646 return 1
1647
1648 # If we had an issue, commit the current state and register the issue.
1649 if not nocommit:
1650 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1651 'patch from issue %(i)s at patchset '
1652 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1653 % {'i': self.GetIssue(), 'p': patchset})])
1654 self.SetIssue(self.GetIssue())
1655 self.SetPatchset(patchset)
1656 print "Committed patch locally."
1657 else:
1658 print "Patch applied to index."
1659 return 0
1660
1661 @staticmethod
1662 def ParseIssueURL(parsed_url):
1663 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1664 return None
1665 # Typical url: https://domain/<issue_number>[/[other]]
1666 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1667 if match:
1668 return _RietveldParsedIssueNumberArgument(
1669 issue=int(match.group(1)),
1670 hostname=parsed_url.netloc)
1671 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1672 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1673 if match:
1674 return _RietveldParsedIssueNumberArgument(
1675 issue=int(match.group(1)),
1676 patchset=int(match.group(2)),
1677 hostname=parsed_url.netloc,
1678 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1679 return None
1680
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001681
1682class _GerritChangelistImpl(_ChangelistCodereviewBase):
1683 def __init__(self, changelist, auth_config=None):
1684 # auth_config is Rietveld thing, kept here to preserve interface only.
1685 super(_GerritChangelistImpl, self).__init__(changelist)
1686 self._change_id = None
1687 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1688 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1689
1690 def _GetGerritHost(self):
1691 # Lazy load of configs.
1692 self.GetCodereviewServer()
1693 return self._gerrit_host
1694
1695 def GetCodereviewServer(self):
1696 if not self._gerrit_server:
1697 # If we're on a branch then get the server potentially associated
1698 # with that branch.
1699 if self.GetIssue():
1700 gerrit_server_setting = self.GetCodereviewServerSetting()
1701 if gerrit_server_setting:
1702 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1703 error_ok=True).strip()
1704 if self._gerrit_server:
1705 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1706 if not self._gerrit_server:
1707 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1708 # has "-review" suffix for lowest level subdomain.
1709 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1710 parts[0] = parts[0] + '-review'
1711 self._gerrit_host = '.'.join(parts)
1712 self._gerrit_server = 'https://%s' % self._gerrit_host
1713 return self._gerrit_server
1714
1715 @staticmethod
1716 def IssueSetting(branch):
1717 return 'branch.%s.gerritissue' % branch
1718
1719 def PatchsetSetting(self):
1720 """Return the git setting that stores this change's most recent patchset."""
1721 return 'branch.%s.gerritpatchset' % self.GetBranch()
1722
1723 def GetCodereviewServerSetting(self):
1724 """Returns the git setting that stores this change's Gerrit server."""
1725 branch = self.GetBranch()
1726 if branch:
1727 return 'branch.%s.gerritserver' % branch
1728 return None
1729
1730 def GetRieveldObjForPresubmit(self):
1731 class ThisIsNotRietveldIssue(object):
1732 def __nonzero__(self):
1733 # This is a hack to make presubmit_support think that rietveld is not
1734 # defined, yet still ensure that calls directly result in a decent
1735 # exception message below.
1736 return False
1737
1738 def __getattr__(self, attr):
1739 print(
1740 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1741 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1742 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1743 'or use Rietveld for codereview.\n'
1744 'See also http://crbug.com/579160.' % attr)
1745 raise NotImplementedError()
1746 return ThisIsNotRietveldIssue()
1747
1748 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001749 """Apply a rough heuristic to give a simple summary of an issue's review
1750 or CQ status, assuming adherence to a common workflow.
1751
1752 Returns None if no issue for this branch, or one of the following keywords:
1753 * 'error' - error from review tool (including deleted issues)
1754 * 'unsent' - no reviewers added
1755 * 'waiting' - waiting for review
1756 * 'reply' - waiting for owner to reply to review
1757 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1758 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1759 * 'commit' - in the commit queue
1760 * 'closed' - abandoned
1761 """
1762 if not self.GetIssue():
1763 return None
1764
1765 try:
1766 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1767 except httplib.HTTPException:
1768 return 'error'
1769
1770 if data['status'] == 'ABANDONED':
1771 return 'closed'
1772
1773 cq_label = data['labels'].get('Commit-Queue', {})
1774 if cq_label:
1775 # Vote value is a stringified integer, which we expect from 0 to 2.
1776 vote_value = cq_label.get('value', '0')
1777 vote_text = cq_label.get('values', {}).get(vote_value, '')
1778 if vote_text.lower() == 'commit':
1779 return 'commit'
1780
1781 lgtm_label = data['labels'].get('Code-Review', {})
1782 if lgtm_label:
1783 if 'rejected' in lgtm_label:
1784 return 'not lgtm'
1785 if 'approved' in lgtm_label:
1786 return 'lgtm'
1787
1788 if not data.get('reviewers', {}).get('REVIEWER', []):
1789 return 'unsent'
1790
1791 messages = data.get('messages', [])
1792 if messages:
1793 owner = data['owner'].get('_account_id')
1794 last_message_author = messages[-1].get('author', {}).get('_account_id')
1795 if owner != last_message_author:
1796 # Some reply from non-owner.
1797 return 'reply'
1798
1799 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001800
1801 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001802 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001803 return data['revisions'][data['current_revision']]['_number']
1804
1805 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001806 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807 return data['revisions'][data['current_revision']]['commit_with_footers']
1808
1809 def UpdateDescriptionRemote(self, description):
1810 # TODO(tandrii)
1811 raise NotImplementedError()
1812
1813 def CloseIssue(self):
1814 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1815
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001816 def SubmitIssue(self, wait_for_merge=True):
1817 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
1818 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001820 def _GetChangeDetail(self, options):
1821 return gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1822 options)
1823
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001824 def CMDLand(self, force, bypass_hooks, verbose):
1825 if git_common.is_dirty_git_tree('land'):
1826 return 1
1827 differs = True
1828 last_upload = RunGit(['config',
1829 'branch.%s.gerritsquashhash' % self.GetBranch()],
1830 error_ok=True).strip()
1831 # Note: git diff outputs nothing if there is no diff.
1832 if not last_upload or RunGit(['diff', last_upload]).strip():
1833 print('WARNING: some changes from local branch haven\'t been uploaded')
1834 else:
1835 detail = self._GetChangeDetail(['CURRENT_REVISION'])
1836 if detail['current_revision'] == last_upload:
1837 differs = False
1838 else:
1839 print('WARNING: local branch contents differ from latest uploaded '
1840 'patchset')
1841 if differs:
1842 if not force:
1843 ask_for_data(
1844 'Do you want to submit latest Gerrit patchset and bypass hooks?')
1845 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
1846 elif not bypass_hooks:
1847 hook_results = self.RunHook(
1848 committing=True,
1849 may_prompt=not force,
1850 verbose=verbose,
1851 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
1852 if not hook_results.should_continue():
1853 return 1
1854
1855 self.SubmitIssue(wait_for_merge=True)
1856 print('Issue %s has been submitted.' % self.GetIssueURL())
1857 return 0
1858
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001859 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1860 directory):
1861 assert not reject
1862 assert not nocommit
1863 assert not directory
1864 assert parsed_issue_arg.valid
1865
1866 self._changelist.issue = parsed_issue_arg.issue
1867
1868 if parsed_issue_arg.hostname:
1869 self._gerrit_host = parsed_issue_arg.hostname
1870 self._gerrit_server = 'https://%s' % self._gerrit_host
1871
1872 detail = self._GetChangeDetail(['ALL_REVISIONS'])
1873
1874 if not parsed_issue_arg.patchset:
1875 # Use current revision by default.
1876 revision_info = detail['revisions'][detail['current_revision']]
1877 patchset = int(revision_info['_number'])
1878 else:
1879 patchset = parsed_issue_arg.patchset
1880 for revision_info in detail['revisions'].itervalues():
1881 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1882 break
1883 else:
1884 DieWithError('Couldn\'t find patchset %i in issue %i' %
1885 (parsed_issue_arg.patchset, self.GetIssue()))
1886
1887 fetch_info = revision_info['fetch']['http']
1888 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
1889 RunGit(['cherry-pick', 'FETCH_HEAD'])
1890 self.SetIssue(self.GetIssue())
1891 self.SetPatchset(patchset)
1892 print('Committed patch for issue %i pathset %i locally' %
1893 (self.GetIssue(), self.GetPatchset()))
1894 return 0
1895
1896 @staticmethod
1897 def ParseIssueURL(parsed_url):
1898 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1899 return None
1900 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
1901 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
1902 # Short urls like https://domain/<issue_number> can be used, but don't allow
1903 # specifying the patchset (you'd 404), but we allow that here.
1904 if parsed_url.path == '/':
1905 part = parsed_url.fragment
1906 else:
1907 part = parsed_url.path
1908 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
1909 if match:
1910 return _ParsedIssueNumberArgument(
1911 issue=int(match.group(2)),
1912 patchset=int(match.group(4)) if match.group(4) else None,
1913 hostname=parsed_url.netloc)
1914 return None
1915
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001916
1917_CODEREVIEW_IMPLEMENTATIONS = {
1918 'rietveld': _RietveldChangelistImpl,
1919 'gerrit': _GerritChangelistImpl,
1920}
1921
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001922
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001923class ChangeDescription(object):
1924 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001925 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001926 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001927
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001928 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001929 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001930
agable@chromium.org42c20792013-09-12 17:34:49 +00001931 @property # www.logilab.org/ticket/89786
1932 def description(self): # pylint: disable=E0202
1933 return '\n'.join(self._description_lines)
1934
1935 def set_description(self, desc):
1936 if isinstance(desc, basestring):
1937 lines = desc.splitlines()
1938 else:
1939 lines = [line.rstrip() for line in desc]
1940 while lines and not lines[0]:
1941 lines.pop(0)
1942 while lines and not lines[-1]:
1943 lines.pop(-1)
1944 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001945
piman@chromium.org336f9122014-09-04 02:16:55 +00001946 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001947 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001948 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001949 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001950 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001951 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001952
agable@chromium.org42c20792013-09-12 17:34:49 +00001953 # Get the set of R= and TBR= lines and remove them from the desciption.
1954 regexp = re.compile(self.R_LINE)
1955 matches = [regexp.match(line) for line in self._description_lines]
1956 new_desc = [l for i, l in enumerate(self._description_lines)
1957 if not matches[i]]
1958 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001959
agable@chromium.org42c20792013-09-12 17:34:49 +00001960 # Construct new unified R= and TBR= lines.
1961 r_names = []
1962 tbr_names = []
1963 for match in matches:
1964 if not match:
1965 continue
1966 people = cleanup_list([match.group(2).strip()])
1967 if match.group(1) == 'TBR':
1968 tbr_names.extend(people)
1969 else:
1970 r_names.extend(people)
1971 for name in r_names:
1972 if name not in reviewers:
1973 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001974 if add_owners_tbr:
1975 owners_db = owners.Database(change.RepositoryRoot(),
1976 fopen=file, os_path=os.path, glob=glob.glob)
1977 all_reviewers = set(tbr_names + reviewers)
1978 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1979 all_reviewers)
1980 tbr_names.extend(owners_db.reviewers_for(missing_files,
1981 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001982 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1983 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1984
1985 # Put the new lines in the description where the old first R= line was.
1986 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1987 if 0 <= line_loc < len(self._description_lines):
1988 if new_tbr_line:
1989 self._description_lines.insert(line_loc, new_tbr_line)
1990 if new_r_line:
1991 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001992 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001993 if new_r_line:
1994 self.append_footer(new_r_line)
1995 if new_tbr_line:
1996 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001997
1998 def prompt(self):
1999 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002000 self.set_description([
2001 '# Enter a description of the change.',
2002 '# This will be displayed on the codereview site.',
2003 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002004 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002005 '--------------------',
2006 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002007
agable@chromium.org42c20792013-09-12 17:34:49 +00002008 regexp = re.compile(self.BUG_LINE)
2009 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002010 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002011 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002012 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002013 if not content:
2014 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002015 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002016
2017 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002018 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2019 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002020 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002021 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002022
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002023 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002024 if self._description_lines:
2025 # Add an empty line if either the last line or the new line isn't a tag.
2026 last_line = self._description_lines[-1]
2027 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2028 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2029 self._description_lines.append('')
2030 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002031
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002032 def get_reviewers(self):
2033 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002034 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2035 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002036 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002037
2038
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002039def get_approving_reviewers(props):
2040 """Retrieves the reviewers that approved a CL from the issue properties with
2041 messages.
2042
2043 Note that the list may contain reviewers that are not committer, thus are not
2044 considered by the CQ.
2045 """
2046 return sorted(
2047 set(
2048 message['sender']
2049 for message in props['messages']
2050 if message['approval'] and message['sender'] in props['reviewers']
2051 )
2052 )
2053
2054
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055def FindCodereviewSettingsFile(filename='codereview.settings'):
2056 """Finds the given file starting in the cwd and going up.
2057
2058 Only looks up to the top of the repository unless an
2059 'inherit-review-settings-ok' file exists in the root of the repository.
2060 """
2061 inherit_ok_file = 'inherit-review-settings-ok'
2062 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002063 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2065 root = '/'
2066 while True:
2067 if filename in os.listdir(cwd):
2068 if os.path.isfile(os.path.join(cwd, filename)):
2069 return open(os.path.join(cwd, filename))
2070 if cwd == root:
2071 break
2072 cwd = os.path.dirname(cwd)
2073
2074
2075def LoadCodereviewSettingsFromFile(fileobj):
2076 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002077 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002078
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079 def SetProperty(name, setting, unset_error_ok=False):
2080 fullname = 'rietveld.' + name
2081 if setting in keyvals:
2082 RunGit(['config', fullname, keyvals[setting]])
2083 else:
2084 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2085
2086 SetProperty('server', 'CODE_REVIEW_SERVER')
2087 # Only server setting is required. Other settings can be absent.
2088 # In that case, we ignore errors raised during option deletion attempt.
2089 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002090 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2092 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002093 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002094 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002095 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2096 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002097 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002098 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002099 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002100 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2101 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002103 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002104 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002105
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002106 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2107 RunGit(['config', 'gerrit.squash-uploads',
2108 keyvals['GERRIT_SQUASH_UPLOADS']])
2109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002110 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2111 #should be of the form
2112 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2113 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2114 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2115 keyvals['ORIGIN_URL_CONFIG']])
2116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002118def urlretrieve(source, destination):
2119 """urllib is broken for SSL connections via a proxy therefore we
2120 can't use urllib.urlretrieve()."""
2121 with open(destination, 'w') as f:
2122 f.write(urllib2.urlopen(source).read())
2123
2124
ukai@chromium.org712d6102013-11-27 00:52:58 +00002125def hasSheBang(fname):
2126 """Checks fname is a #! script."""
2127 with open(fname) as f:
2128 return f.read(2).startswith('#!')
2129
2130
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002131# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2132def DownloadHooks(*args, **kwargs):
2133 pass
2134
2135
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002136def DownloadGerritHook(force):
2137 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002138
2139 Args:
2140 force: True to update hooks. False to install hooks if not present.
2141 """
2142 if not settings.GetIsGerrit():
2143 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002144 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002145 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2146 if not os.access(dst, os.X_OK):
2147 if os.path.exists(dst):
2148 if not force:
2149 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002150 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002151 print(
2152 'WARNING: installing Gerrit commit-msg hook.\n'
2153 ' This behavior of git cl will soon be disabled.\n'
2154 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002155 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002156 if not hasSheBang(dst):
2157 DieWithError('Not a script: %s\n'
2158 'You need to download from\n%s\n'
2159 'into .git/hooks/commit-msg and '
2160 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002161 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2162 except Exception:
2163 if os.path.exists(dst):
2164 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002165 DieWithError('\nFailed to download hooks.\n'
2166 'You need to download from\n%s\n'
2167 'into .git/hooks/commit-msg and '
2168 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002169
2170
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002171
2172def GetRietveldCodereviewSettingsInteractively():
2173 """Prompt the user for settings."""
2174 server = settings.GetDefaultServerUrl(error_ok=True)
2175 prompt = 'Rietveld server (host[:port])'
2176 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2177 newserver = ask_for_data(prompt + ':')
2178 if not server and not newserver:
2179 newserver = DEFAULT_SERVER
2180 if newserver:
2181 newserver = gclient_utils.UpgradeToHttps(newserver)
2182 if newserver != server:
2183 RunGit(['config', 'rietveld.server', newserver])
2184
2185 def SetProperty(initial, caption, name, is_url):
2186 prompt = caption
2187 if initial:
2188 prompt += ' ("x" to clear) [%s]' % initial
2189 new_val = ask_for_data(prompt + ':')
2190 if new_val == 'x':
2191 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2192 elif new_val:
2193 if is_url:
2194 new_val = gclient_utils.UpgradeToHttps(new_val)
2195 if new_val != initial:
2196 RunGit(['config', 'rietveld.' + name, new_val])
2197
2198 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2199 SetProperty(settings.GetDefaultPrivateFlag(),
2200 'Private flag (rietveld only)', 'private', False)
2201 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2202 'tree-status-url', False)
2203 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2204 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2205 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2206 'run-post-upload-hook', False)
2207
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002208@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002209def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002210 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002211
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002212 print('WARNING: git cl config works for Rietveld only.\n'
2213 'For Gerrit, see http://crbug.com/579160.')
2214 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002215 parser.add_option('--activate-update', action='store_true',
2216 help='activate auto-updating [rietveld] section in '
2217 '.git/config')
2218 parser.add_option('--deactivate-update', action='store_true',
2219 help='deactivate auto-updating [rietveld] section in '
2220 '.git/config')
2221 options, args = parser.parse_args(args)
2222
2223 if options.deactivate_update:
2224 RunGit(['config', 'rietveld.autoupdate', 'false'])
2225 return
2226
2227 if options.activate_update:
2228 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2229 return
2230
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002231 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002232 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002233 return 0
2234
2235 url = args[0]
2236 if not url.endswith('codereview.settings'):
2237 url = os.path.join(url, 'codereview.settings')
2238
2239 # Load code review settings and download hooks (if available).
2240 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2241 return 0
2242
2243
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002244def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002245 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002246 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2247 branch = ShortBranchName(branchref)
2248 _, args = parser.parse_args(args)
2249 if not args:
2250 print("Current base-url:")
2251 return RunGit(['config', 'branch.%s.base-url' % branch],
2252 error_ok=False).strip()
2253 else:
2254 print("Setting base-url to %s" % args[0])
2255 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2256 error_ok=False).strip()
2257
2258
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002259def color_for_status(status):
2260 """Maps a Changelist status to color, for CMDstatus and other tools."""
2261 return {
2262 'unsent': Fore.RED,
2263 'waiting': Fore.BLUE,
2264 'reply': Fore.YELLOW,
2265 'lgtm': Fore.GREEN,
2266 'commit': Fore.MAGENTA,
2267 'closed': Fore.CYAN,
2268 'error': Fore.WHITE,
2269 }.get(status, Fore.WHITE)
2270
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002271def fetch_cl_status(branch, auth_config=None):
2272 """Fetches information for an issue and returns (branch, issue, status)."""
2273 cl = Changelist(branchref=branch, auth_config=auth_config)
2274 url = cl.GetIssueURL()
2275 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002276
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002277 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002278 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002279 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002280
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002281 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002282
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002283def get_cl_statuses(
2284 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002285 """Returns a blocking iterable of (branch, issue, color) for given branches.
2286
2287 If fine_grained is true, this will fetch CL statuses from the server.
2288 Otherwise, simply indicate if there's a matching url for the given branches.
2289
2290 If max_processes is specified, it is used as the maximum number of processes
2291 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2292 spawned.
2293 """
2294 # Silence upload.py otherwise it becomes unwieldly.
2295 upload.verbosity = 0
2296
2297 if fine_grained:
2298 # Process one branch synchronously to work through authentication, then
2299 # spawn processes to process all the other branches in parallel.
2300 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002301 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2302 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002303
2304 branches_to_fetch = branches[1:]
2305 pool = ThreadPool(
2306 min(max_processes, len(branches_to_fetch))
2307 if max_processes is not None
2308 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002309 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002310 yield x
2311 else:
2312 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2313 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002314 cl = Changelist(branchref=b, auth_config=auth_config)
2315 url = cl.GetIssueURL()
2316 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002317
rmistry@google.com2dd99862015-06-22 12:22:18 +00002318
2319def upload_branch_deps(cl, args):
2320 """Uploads CLs of local branches that are dependents of the current branch.
2321
2322 If the local branch dependency tree looks like:
2323 test1 -> test2.1 -> test3.1
2324 -> test3.2
2325 -> test2.2 -> test3.3
2326
2327 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2328 run on the dependent branches in this order:
2329 test2.1, test3.1, test3.2, test2.2, test3.3
2330
2331 Note: This function does not rebase your local dependent branches. Use it when
2332 you make a change to the parent branch that will not conflict with its
2333 dependent branches, and you would like their dependencies updated in
2334 Rietveld.
2335 """
2336 if git_common.is_dirty_git_tree('upload-branch-deps'):
2337 return 1
2338
2339 root_branch = cl.GetBranch()
2340 if root_branch is None:
2341 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2342 'Get on a branch!')
2343 if not cl.GetIssue() or not cl.GetPatchset():
2344 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2345 'patchset dependencies without an uploaded CL.')
2346
2347 branches = RunGit(['for-each-ref',
2348 '--format=%(refname:short) %(upstream:short)',
2349 'refs/heads'])
2350 if not branches:
2351 print('No local branches found.')
2352 return 0
2353
2354 # Create a dictionary of all local branches to the branches that are dependent
2355 # on it.
2356 tracked_to_dependents = collections.defaultdict(list)
2357 for b in branches.splitlines():
2358 tokens = b.split()
2359 if len(tokens) == 2:
2360 branch_name, tracked = tokens
2361 tracked_to_dependents[tracked].append(branch_name)
2362
2363 print
2364 print 'The dependent local branches of %s are:' % root_branch
2365 dependents = []
2366 def traverse_dependents_preorder(branch, padding=''):
2367 dependents_to_process = tracked_to_dependents.get(branch, [])
2368 padding += ' '
2369 for dependent in dependents_to_process:
2370 print '%s%s' % (padding, dependent)
2371 dependents.append(dependent)
2372 traverse_dependents_preorder(dependent, padding)
2373 traverse_dependents_preorder(root_branch)
2374 print
2375
2376 if not dependents:
2377 print 'There are no dependent local branches for %s' % root_branch
2378 return 0
2379
2380 print ('This command will checkout all dependent branches and run '
2381 '"git cl upload".')
2382 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2383
andybons@chromium.org962f9462016-02-03 20:00:42 +00002384 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002385 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002386 args.extend(['-t', 'Updated patchset dependency'])
2387
rmistry@google.com2dd99862015-06-22 12:22:18 +00002388 # Record all dependents that failed to upload.
2389 failures = {}
2390 # Go through all dependents, checkout the branch and upload.
2391 try:
2392 for dependent_branch in dependents:
2393 print
2394 print '--------------------------------------'
2395 print 'Running "git cl upload" from %s:' % dependent_branch
2396 RunGit(['checkout', '-q', dependent_branch])
2397 print
2398 try:
2399 if CMDupload(OptionParser(), args) != 0:
2400 print 'Upload failed for %s!' % dependent_branch
2401 failures[dependent_branch] = 1
2402 except: # pylint: disable=W0702
2403 failures[dependent_branch] = 1
2404 print
2405 finally:
2406 # Swap back to the original root branch.
2407 RunGit(['checkout', '-q', root_branch])
2408
2409 print
2410 print 'Upload complete for dependent branches!'
2411 for dependent_branch in dependents:
2412 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2413 print ' %s : %s' % (dependent_branch, upload_status)
2414 print
2415
2416 return 0
2417
2418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002419def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002420 """Show status of changelists.
2421
2422 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002423 - Red not sent for review or broken
2424 - Blue waiting for review
2425 - Yellow waiting for you to reply to review
2426 - Green LGTM'ed
2427 - Magenta in the commit queue
2428 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002429
2430 Also see 'git cl comments'.
2431 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002432 parser.add_option('--field',
2433 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002434 parser.add_option('-f', '--fast', action='store_true',
2435 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002436 parser.add_option(
2437 '-j', '--maxjobs', action='store', type=int,
2438 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002439
2440 auth.add_auth_options(parser)
2441 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002442 if args:
2443 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002444 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002445
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002446 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002447 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002448 if options.field.startswith('desc'):
2449 print cl.GetDescription()
2450 elif options.field == 'id':
2451 issueid = cl.GetIssue()
2452 if issueid:
2453 print issueid
2454 elif options.field == 'patch':
2455 patchset = cl.GetPatchset()
2456 if patchset:
2457 print patchset
2458 elif options.field == 'url':
2459 url = cl.GetIssueURL()
2460 if url:
2461 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002462 return 0
2463
2464 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2465 if not branches:
2466 print('No local branch found.')
2467 return 0
2468
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002469 changes = (
2470 Changelist(branchref=b, auth_config=auth_config)
2471 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002472 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002473 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002474 alignment = max(5, max(len(b) for b in branches))
2475 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002476 output = get_cl_statuses(branches,
2477 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002478 max_processes=options.maxjobs,
2479 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002480
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002481 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002482 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002483 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002484 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002485 b, i, status = output.next()
2486 branch_statuses[b] = (i, status)
2487 issue_url, status = branch_statuses.pop(branch)
2488 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002489 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00002490 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00002491 color = ''
2492 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002493 status_str = '(%s)' % status if status else ''
2494 print ' %*s : %s%s %s%s' % (
2495 alignment, ShortBranchName(branch), color, issue_url, status_str,
2496 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002497
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002498 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002499 print
2500 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002501 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002502 if not cl.GetIssue():
2503 print 'No issue assigned.'
2504 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002505 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002506 if not options.fast:
2507 print 'Issue description:'
2508 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002509 return 0
2510
2511
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002512def colorize_CMDstatus_doc():
2513 """To be called once in main() to add colors to git cl status help."""
2514 colors = [i for i in dir(Fore) if i[0].isupper()]
2515
2516 def colorize_line(line):
2517 for color in colors:
2518 if color in line.upper():
2519 # Extract whitespaces first and the leading '-'.
2520 indent = len(line) - len(line.lstrip(' ')) + 1
2521 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2522 return line
2523
2524 lines = CMDstatus.__doc__.splitlines()
2525 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2526
2527
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002528@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002529def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002530 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002531
2532 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002533 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002534 parser.add_option('-r', '--reverse', action='store_true',
2535 help='Lookup the branch(es) for the specified issues. If '
2536 'no issues are specified, all branches with mapped '
2537 'issues will be listed.')
2538 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002539
dnj@chromium.org406c4402015-03-03 17:22:28 +00002540 if options.reverse:
2541 branches = RunGit(['for-each-ref', 'refs/heads',
2542 '--format=%(refname:short)']).splitlines()
2543
2544 # Reverse issue lookup.
2545 issue_branch_map = {}
2546 for branch in branches:
2547 cl = Changelist(branchref=branch)
2548 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2549 if not args:
2550 args = sorted(issue_branch_map.iterkeys())
2551 for issue in args:
2552 if not issue:
2553 continue
2554 print 'Branch for issue number %s: %s' % (
2555 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2556 else:
2557 cl = Changelist()
2558 if len(args) > 0:
2559 try:
2560 issue = int(args[0])
2561 except ValueError:
2562 DieWithError('Pass a number to set the issue or none to list it.\n'
2563 'Maybe you want to run git cl status?')
2564 cl.SetIssue(issue)
2565 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002566 return 0
2567
2568
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002569def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002570 """Shows or posts review comments for any changelist."""
2571 parser.add_option('-a', '--add-comment', dest='comment',
2572 help='comment to add to an issue')
2573 parser.add_option('-i', dest='issue',
2574 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002575 parser.add_option('-j', '--json-file',
2576 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002577 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002578 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002579 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002580
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002581 issue = None
2582 if options.issue:
2583 try:
2584 issue = int(options.issue)
2585 except ValueError:
2586 DieWithError('A review issue id is expected to be a number')
2587
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002588 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002589
2590 if options.comment:
2591 cl.AddComment(options.comment)
2592 return 0
2593
2594 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002595 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002596 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002597 summary.append({
2598 'date': message['date'],
2599 'lgtm': False,
2600 'message': message['text'],
2601 'not_lgtm': False,
2602 'sender': message['sender'],
2603 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002604 if message['disapproval']:
2605 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002606 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002607 elif message['approval']:
2608 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002609 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002610 elif message['sender'] == data['owner_email']:
2611 color = Fore.MAGENTA
2612 else:
2613 color = Fore.BLUE
2614 print '\n%s%s %s%s' % (
2615 color, message['date'].split('.', 1)[0], message['sender'],
2616 Fore.RESET)
2617 if message['text'].strip():
2618 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002619 if options.json_file:
2620 with open(options.json_file, 'wb') as f:
2621 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002622 return 0
2623
2624
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002625def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002626 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002627 parser.add_option('-d', '--display', action='store_true',
2628 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002629 auth.add_auth_options(parser)
2630 options, _ = parser.parse_args(args)
2631 auth_config = auth.extract_auth_config_from_options(options)
2632 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002633 if not cl.GetIssue():
2634 DieWithError('This branch has no associated changelist.')
2635 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002636 if options.display:
2637 print description.description
2638 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002639 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002640 if cl.GetDescription() != description.description:
2641 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002642 return 0
2643
2644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002645def CreateDescriptionFromLog(args):
2646 """Pulls out the commit log to use as a base for the CL description."""
2647 log_args = []
2648 if len(args) == 1 and not args[0].endswith('.'):
2649 log_args = [args[0] + '..']
2650 elif len(args) == 1 and args[0].endswith('...'):
2651 log_args = [args[0][:-1]]
2652 elif len(args) == 2:
2653 log_args = [args[0] + '..' + args[1]]
2654 else:
2655 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002656 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002657
2658
thestig@chromium.org44202a22014-03-11 19:22:18 +00002659def CMDlint(parser, args):
2660 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002661 parser.add_option('--filter', action='append', metavar='-x,+y',
2662 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002663 auth.add_auth_options(parser)
2664 options, args = parser.parse_args(args)
2665 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002666
2667 # Access to a protected member _XX of a client class
2668 # pylint: disable=W0212
2669 try:
2670 import cpplint
2671 import cpplint_chromium
2672 except ImportError:
2673 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2674 return 1
2675
2676 # Change the current working directory before calling lint so that it
2677 # shows the correct base.
2678 previous_cwd = os.getcwd()
2679 os.chdir(settings.GetRoot())
2680 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002681 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002682 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2683 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002684 if not files:
2685 print "Cannot lint an empty CL"
2686 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002687
2688 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002689 command = args + files
2690 if options.filter:
2691 command = ['--filter=' + ','.join(options.filter)] + command
2692 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002693
2694 white_regex = re.compile(settings.GetLintRegex())
2695 black_regex = re.compile(settings.GetLintIgnoreRegex())
2696 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2697 for filename in filenames:
2698 if white_regex.match(filename):
2699 if black_regex.match(filename):
2700 print "Ignoring file %s" % filename
2701 else:
2702 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2703 extra_check_functions)
2704 else:
2705 print "Skipping file %s" % filename
2706 finally:
2707 os.chdir(previous_cwd)
2708 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2709 if cpplint._cpplint_state.error_count != 0:
2710 return 1
2711 return 0
2712
2713
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002715 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002716 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002718 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002719 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002720 auth.add_auth_options(parser)
2721 options, args = parser.parse_args(args)
2722 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002723
sbc@chromium.org71437c02015-04-09 19:29:40 +00002724 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002725 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002726 return 1
2727
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002728 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729 if args:
2730 base_branch = args[0]
2731 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002732 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002733 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002734
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002735 cl.RunHook(
2736 committing=not options.upload,
2737 may_prompt=False,
2738 verbose=options.verbose,
2739 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002740 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002741
2742
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002743def AddChangeIdToCommitMessage(options, args):
2744 """Re-commits using the current message, assumes the commit hook is in
2745 place.
2746 """
2747 log_desc = options.message or CreateDescriptionFromLog(args)
2748 git_command = ['commit', '--amend', '-m', log_desc]
2749 RunGit(git_command)
2750 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002751 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002752 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002753 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002754 else:
2755 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2756
2757
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002758def GenerateGerritChangeId(message):
2759 """Returns Ixxxxxx...xxx change id.
2760
2761 Works the same way as
2762 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2763 but can be called on demand on all platforms.
2764
2765 The basic idea is to generate git hash of a state of the tree, original commit
2766 message, author/committer info and timestamps.
2767 """
2768 lines = []
2769 tree_hash = RunGitSilent(['write-tree'])
2770 lines.append('tree %s' % tree_hash.strip())
2771 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2772 if code == 0:
2773 lines.append('parent %s' % parent.strip())
2774 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2775 lines.append('author %s' % author.strip())
2776 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2777 lines.append('committer %s' % committer.strip())
2778 lines.append('')
2779 # Note: Gerrit's commit-hook actually cleans message of some lines and
2780 # whitespace. This code is not doing this, but it clearly won't decrease
2781 # entropy.
2782 lines.append(message)
2783 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2784 stdin='\n'.join(lines))
2785 return 'I%s' % change_hash.strip()
2786
2787
piman@chromium.org336f9122014-09-04 02:16:55 +00002788def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002789 """upload the current branch to gerrit."""
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002790 # TODO(tandrii): refactor this to be a method of _GerritChangelistImpl,
2791 # to avoid private members accessors below.
2792
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002793 # We assume the remote called "origin" is the one we want.
2794 # It is probably not worthwhile to support different workflows.
2795 gerrit_remote = 'origin'
2796
luqui@chromium.org609f3952015-05-04 22:47:04 +00002797 remote, remote_branch = cl.GetRemoteBranch()
2798 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2799 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002800
andybons@chromium.org962f9462016-02-03 20:00:42 +00002801 if options.title:
2802 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002804
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002805 if options.squash:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002806 if not cl.GetIssue():
2807 # TODO(tandrii): deperecate this after 2016Q2.
2808 # Backwards compatibility with shadow branch, which used to contain
2809 # change-id for a given branch, using which we can fetch actual issue
2810 # number and set it as the property of the branch, which is the new way.
2811 message = RunGitSilent(['show', '--format=%B', '-s',
2812 'refs/heads/git_cl_uploads/%s' % cl.GetBranch()])
2813 if message:
2814 change_ids = git_footers.get_footer_change_id(message.strip())
2815 if change_ids and len(change_ids) == 1:
2816 details = gerrit_util.GetChangeDetail(
2817 cl._codereview_impl._GetGerritHost(), change_ids[0])
2818 if details:
2819 print('WARNING: found old upload in branch git_cl_uploads/%s '
2820 'corresponding to issue %s' %
2821 (cl.GetBranch(), details['_number']))
2822 cl.SetIssue(details['_number'])
2823 if not cl.GetIssue():
2824 DieWithError(
2825 '\n' # For readability of the blob below.
2826 'Found old upload in branch git_cl_uploads/%s, '
2827 'but failed to find corresponding Gerrit issue.\n'
2828 'If you know the issue number, set it manually first:\n'
2829 ' git cl issue 123456\n'
2830 'If you intended to upload this CL as new issue, '
2831 'just delete or rename the old upload branch:\n'
2832 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2833 'After that, please run git cl upload again.' %
2834 tuple([cl.GetBranch()] * 3))
2835 # End of backwards compatability.
2836
2837 if cl.GetIssue():
2838 # Try to get the message from a previous upload.
2839 message = cl.GetDescription()
2840 if not message:
2841 DieWithError(
2842 'failed to fetch description from current Gerrit issue %d\n'
2843 '%s' % (cl.GetIssue(), cl.GetIssueURL()))
2844 change_id = cl._codereview_impl._GetChangeDetail([])['change_id']
2845 while True:
2846 footer_change_ids = git_footers.get_footer_change_id(message)
2847 if footer_change_ids == [change_id]:
2848 break
2849 if not footer_change_ids:
2850 message = git_footers.add_footer_change_id(message, change_id)
2851 print('WARNING: appended missing Change-Id to issue description')
2852 continue
2853 # There is already a valid footer but with different or several ids.
2854 # Doing this automatically is non-trivial as we don't want to lose
2855 # existing other footers, yet we want to append just 1 desired
2856 # Change-Id. Thus, just create a new footer, but let user verify the new
2857 # description.
2858 message = '%s\n\nChange-Id: %s' % (message, change_id)
2859 print(
2860 'WARNING: issue %s has Change-Id footer(s):\n'
2861 ' %s\n'
2862 'but issue has Change-Id %s, according to Gerrit.\n'
2863 'Please, check the proposed correction to the description, '
2864 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2865 % (cl.GetIssue(), '\n '.join(footer_change_ids), change_id,
2866 change_id))
2867 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2868 if not options.force:
2869 change_desc = ChangeDescription(message)
2870 change_desc.prompt()
2871 message = change_desc.description
2872 if not message:
2873 DieWithError("Description is empty. Aborting...")
2874 # Continue the while loop.
2875 # Sanity check of this code - we should end up with proper message footer.
2876 assert [change_id] == git_footers.get_footer_change_id(message)
2877 change_desc = ChangeDescription(message)
2878 else:
2879 change_desc = ChangeDescription(
2880 options.message or CreateDescriptionFromLog(args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002881 if not options.force:
2882 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002883 if not change_desc.description:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002884 DieWithError("Description is empty. Aborting...")
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002885 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002886 change_ids = git_footers.get_footer_change_id(message)
2887 if len(change_ids) > 1:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002888 DieWithError('too many Change-Id footers, at most 1 allowed.')
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002889 if not change_ids:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002890 # Generate the Change-Id automatically.
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002891 message = git_footers.add_footer_change_id(
2892 message, GenerateGerritChangeId(message))
2893 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002894 change_ids = git_footers.get_footer_change_id(message)
2895 assert len(change_ids) == 1
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002896 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002897
2898 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2899 if remote is '.':
2900 # If our upstream branch is local, we base our squashed commit on its
2901 # squashed version.
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002902 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2903 # Check the squashed hash of the parent.
2904 parent = RunGit(['config',
2905 'branch.%s.gerritsquashhash' % upstream_branch_name],
2906 error_ok=True).strip()
2907 # Verify that the upstream branch has been uploaded too, otherwise
2908 # Gerrit will create additional CLs when uploading.
2909 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2910 RunGitSilent(['rev-parse', parent + ':'])):
2911 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2912 DieWithError(
2913 'Upload upstream branch %s first.\n'
2914 'Note: maybe you\'ve uploaded it with --no-squash or with an old\n'
2915 ' version of depot_tools. If so, then re-upload it with:\n'
2916 ' git cl upload --squash\n' % upstream_branch_name)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002917 else:
2918 parent = cl.GetCommonAncestorWithUpstream()
2919
2920 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2921 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2922 '-m', message]).strip()
2923 else:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002924 change_desc = ChangeDescription(
2925 options.message or CreateDescriptionFromLog(args))
2926 if not change_desc.description:
2927 DieWithError("Description is empty. Aborting...")
2928
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002929 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002930 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002931 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002932 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002933 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002934 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002935
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002936 assert change_desc
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002937 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2938 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002939 if len(commits) > 1:
2940 print('WARNING: This will upload %d commits. Run the following command '
2941 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002942 print('git log %s..%s' % (parent, ref_to_push))
2943 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002944 'commit.')
2945 ask_for_data('About to upload; enter to confirm.')
2946
piman@chromium.org336f9122014-09-04 02:16:55 +00002947 if options.reviewers or options.tbr_owners:
2948 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949
ukai@chromium.orge8077812012-02-03 03:41:46 +00002950 receive_options = []
2951 cc = cl.GetCCList().split(',')
2952 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002953 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002954 cc = filter(None, cc)
2955 if cc:
2956 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002957 if change_desc.get_reviewers():
2958 receive_options.extend(
2959 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002960
ukai@chromium.orge8077812012-02-03 03:41:46 +00002961 git_command = ['push']
2962 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002963 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002964 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002965 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002966 push_stdout = gclient_utils.CheckCallAndFilter(
2967 ['git'] + git_command,
2968 print_stdout=True,
2969 # Flush after every line: useful for seeing progress when running as
2970 # recipe.
2971 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002972
2973 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002974 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2975 change_numbers = [m.group(1)
2976 for m in map(regex.match, push_stdout.splitlines())
2977 if m]
2978 if len(change_numbers) != 1:
2979 DieWithError(
2980 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2981 'Change-Id: %s') % (len(change_numbers), change_id))
2982 cl.SetIssue(change_numbers[0])
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002983 RunGit(['config', 'branch.%s.gerritsquashhash' % cl.GetBranch(),
2984 ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002985 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002986
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002987
wittman@chromium.org455dc922015-01-26 20:15:50 +00002988def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2989 """Computes the remote branch ref to use for the CL.
2990
2991 Args:
2992 remote (str): The git remote for the CL.
2993 remote_branch (str): The git remote branch for the CL.
2994 target_branch (str): The target branch specified by the user.
2995 pending_prefix (str): The pending prefix from the settings.
2996 """
2997 if not (remote and remote_branch):
2998 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002999
wittman@chromium.org455dc922015-01-26 20:15:50 +00003000 if target_branch:
3001 # Cannonicalize branch references to the equivalent local full symbolic
3002 # refs, which are then translated into the remote full symbolic refs
3003 # below.
3004 if '/' not in target_branch:
3005 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3006 else:
3007 prefix_replacements = (
3008 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3009 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3010 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3011 )
3012 match = None
3013 for regex, replacement in prefix_replacements:
3014 match = re.search(regex, target_branch)
3015 if match:
3016 remote_branch = target_branch.replace(match.group(0), replacement)
3017 break
3018 if not match:
3019 # This is a branch path but not one we recognize; use as-is.
3020 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003021 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3022 # Handle the refs that need to land in different refs.
3023 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003024
wittman@chromium.org455dc922015-01-26 20:15:50 +00003025 # Create the true path to the remote branch.
3026 # Does the following translation:
3027 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3028 # * refs/remotes/origin/master -> refs/heads/master
3029 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3030 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3031 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3032 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3033 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3034 'refs/heads/')
3035 elif remote_branch.startswith('refs/remotes/branch-heads'):
3036 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3037 # If a pending prefix exists then replace refs/ with it.
3038 if pending_prefix:
3039 remote_branch = remote_branch.replace('refs/', pending_prefix)
3040 return remote_branch
3041
3042
piman@chromium.org336f9122014-09-04 02:16:55 +00003043def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003044 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003045 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003046 upload_args.extend(['--server', cl.GetCodereviewServer()])
3047 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
3048 upload_args.extend(auth.auth_config_to_command_options(
3049 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003050 if options.emulate_svn_auto_props:
3051 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003052
3053 change_desc = None
3054
pgervais@chromium.org91141372014-01-09 23:27:20 +00003055 if options.email is not None:
3056 upload_args.extend(['--email', options.email])
3057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003058 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003059 if options.title:
3060 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00003061 if options.message:
3062 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00003063 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003064 print ("This branch is associated with issue %s. "
3065 "Adding patch to that issue." % cl.GetIssue())
3066 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003067 if options.title:
3068 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00003069 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003070 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00003071 if options.reviewers or options.tbr_owners:
3072 change_desc.update_reviewers(options.reviewers,
3073 options.tbr_owners,
3074 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003075 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003076 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003077
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003078 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003079 print "Description is empty; aborting."
3080 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003081
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003082 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083 if change_desc.get_reviewers():
3084 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00003085 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003086 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00003087 DieWithError("Must specify reviewers to send email.")
3088 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00003089
3090 # We check this before applying rietveld.private assuming that in
3091 # rietveld.cc only addresses which we can send private CLs to are listed
3092 # if rietveld.private is set, and so we should ignore rietveld.cc only when
3093 # --private is specified explicitly on the command line.
3094 if options.private:
3095 logging.warn('rietveld.cc is ignored since private flag is specified. '
3096 'You need to review and add them manually if necessary.')
3097 cc = cl.GetCCListWithoutDefault()
3098 else:
3099 cc = cl.GetCCList()
3100 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00003101 if cc:
3102 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003103
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003104 if options.private or settings.GetDefaultPrivateFlag() == "True":
3105 upload_args.append('--private')
3106
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003107 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00003108 if not options.find_copies:
3109 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003110
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003111 # Include the upstream repo's URL in the change -- this is useful for
3112 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003113 remote_url = cl.GetGitBaseUrlFromConfig()
3114 if not remote_url:
3115 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003116 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003117 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00003118 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
3119 remote_url = (cl.GetRemoteUrl() + '@'
3120 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003121 if remote_url:
3122 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00003123 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00003124 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
3125 settings.GetPendingRefPrefix())
3126 if target_ref:
3127 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003128
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003129 # Look for dependent patchsets. See crbug.com/480453 for more details.
3130 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3131 upstream_branch = ShortBranchName(upstream_branch)
3132 if remote is '.':
3133 # A local branch is being tracked.
3134 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00003135 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003136 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003137 print ('Skipping dependency patchset upload because git config '
3138 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003139 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003140 else:
3141 auth_config = auth.extract_auth_config_from_options(options)
3142 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
3143 branch_cl_issue_url = branch_cl.GetIssueURL()
3144 branch_cl_issue = branch_cl.GetIssue()
3145 branch_cl_patchset = branch_cl.GetPatchset()
3146 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
3147 upload_args.extend(
3148 ['--depends_on_patchset', '%s:%s' % (
3149 branch_cl_issue, branch_cl_patchset)])
3150 print
3151 print ('The current branch (%s) is tracking a local branch (%s) with '
3152 'an associated CL.') % (cl.GetBranch(), local_branch)
3153 print 'Adding %s/#ps%s as a dependency patchset.' % (
3154 branch_cl_issue_url, branch_cl_patchset)
3155 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003156
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003157 project = settings.GetProject()
3158 if project:
3159 upload_args.extend(['--project', project])
3160
rmistry@google.comef966222015-04-07 11:15:01 +00003161 if options.cq_dry_run:
3162 upload_args.extend(['--cq_dry_run'])
3163
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003164 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00003165 upload_args = ['upload'] + upload_args + args
3166 logging.info('upload.RealMain(%s)', upload_args)
3167 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00003168 issue = int(issue)
3169 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00003170 except KeyboardInterrupt:
3171 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003172 except:
3173 # If we got an exception after the user typed a description for their
3174 # change, back up the description before re-raising.
3175 if change_desc:
3176 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
3177 print '\nGot exception while uploading -- saving description to %s\n' \
3178 % backup_path
3179 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003180 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 backup_file.close()
3182 raise
3183
3184 if not cl.GetIssue():
3185 cl.SetIssue(issue)
3186 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003187
3188 if options.use_commit_queue:
3189 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003190 return 0
3191
3192
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003193def cleanup_list(l):
3194 """Fixes a list so that comma separated items are put as individual items.
3195
3196 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3197 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3198 """
3199 items = sum((i.split(',') for i in l), [])
3200 stripped_items = (i.strip() for i in items)
3201 return sorted(filter(None, stripped_items))
3202
3203
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003204@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003205def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003206 """Uploads the current changelist to codereview.
3207
3208 Can skip dependency patchset uploads for a branch by running:
3209 git config branch.branch_name.skip-deps-uploads True
3210 To unset run:
3211 git config --unset branch.branch_name.skip-deps-uploads
3212 Can also set the above globally by using the --global flag.
3213 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003214 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3215 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003216 parser.add_option('--bypass-watchlists', action='store_true',
3217 dest='bypass_watchlists',
3218 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003219 parser.add_option('-f', action='store_true', dest='force',
3220 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003221 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003222 parser.add_option('-t', dest='title',
3223 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003224 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003225 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003226 help='reviewer email addresses')
3227 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003228 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003229 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003230 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003231 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003232 parser.add_option('--emulate_svn_auto_props',
3233 '--emulate-svn-auto-props',
3234 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003235 dest="emulate_svn_auto_props",
3236 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003237 parser.add_option('-c', '--use-commit-queue', action='store_true',
3238 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003239 parser.add_option('--private', action='store_true',
3240 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003241 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003242 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003243 metavar='TARGET',
3244 help='Apply CL to remote ref TARGET. ' +
3245 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003246 parser.add_option('--squash', action='store_true',
3247 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003248 parser.add_option('--no-squash', action='store_true',
3249 help='Don\'t squash multiple commits into one ' +
3250 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003251 parser.add_option('--email', default=None,
3252 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003253 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3254 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003255 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3256 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003257 help='Send the patchset to do a CQ dry run right after '
3258 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003259 parser.add_option('--dependencies', action='store_true',
3260 help='Uploads CLs of all the local branches that depend on '
3261 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003262
rmistry@google.com2dd99862015-06-22 12:22:18 +00003263 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003264 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003265 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003266 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003267 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003268
sbc@chromium.org71437c02015-04-09 19:29:40 +00003269 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003270 return 1
3271
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003272 options.reviewers = cleanup_list(options.reviewers)
3273 options.cc = cleanup_list(options.cc)
3274
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003275 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3276 settings.GetIsGerrit()
3277
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003278 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003279 if args:
3280 # TODO(ukai): is it ok for gerrit case?
3281 base_branch = args[0]
3282 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003283 if cl.GetBranch() is None:
3284 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3285
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003286 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003287 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003288 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003289
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003290 # Make sure authenticated to Rietveld before running expensive hooks. It is
3291 # a fast, best efforts check. Rietveld still can reject the authentication
3292 # during the actual upload.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003293 if not cl.IsGerrit() and auth_config.use_oauth2:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003294 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003295 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003296 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003297 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003298
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003299 # Apply watchlists on upload.
3300 change = cl.GetChange(base_branch, None)
3301 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3302 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003303 if not options.bypass_watchlists:
3304 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003305
ukai@chromium.orge8077812012-02-03 03:41:46 +00003306 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003307 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003308 # Set the reviewer list now so that presubmit checks can access it.
3309 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003310 change_description.update_reviewers(options.reviewers,
3311 options.tbr_owners,
3312 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003313 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003314 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003315 may_prompt=not options.force,
3316 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003317 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003318 if not hook_results.should_continue():
3319 return 1
3320 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003321 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003322
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003323 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003324 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003325 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003326 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003327 print ('The last upload made from this repository was patchset #%d but '
3328 'the most recent patchset on the server is #%d.'
3329 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003330 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3331 'from another machine or branch the patch you\'re uploading now '
3332 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003333 ask_for_data('About to upload; enter to confirm.')
3334
iannucci@chromium.org79540052012-10-19 23:15:26 +00003335 print_stats(options.similarity, options.find_copies, args)
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003336 if cl.IsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003337 if options.squash and options.no_squash:
3338 DieWithError('Can only use one of --squash or --no-squash')
3339
3340 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3341 not options.no_squash)
3342
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00003343 ret = GerritUpload(options, args, cl, change)
3344 else:
3345 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003346 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003347 git_set_branch_value('last-upload-hash',
3348 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003349 # Run post upload hooks, if specified.
3350 if settings.GetRunPostUploadHook():
3351 presubmit_support.DoPostUploadExecuter(
3352 change,
3353 cl,
3354 settings.GetRoot(),
3355 options.verbose,
3356 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003357
rmistry@google.com2dd99862015-06-22 12:22:18 +00003358 # Upload all dependencies if specified.
3359 if options.dependencies:
3360 print
3361 print '--dependencies has been specified.'
3362 print 'All dependent local branches will be re-uploaded.'
3363 print
3364 # Remove the dependencies flag from args so that we do not end up in a
3365 # loop.
3366 orig_args.remove('--dependencies')
3367 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003368 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003369
3370
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003371def IsSubmoduleMergeCommit(ref):
3372 # When submodules are added to the repo, we expect there to be a single
3373 # non-git-svn merge commit at remote HEAD with a signature comment.
3374 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003375 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003376 return RunGit(cmd) != ''
3377
3378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003380 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003382 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3383 upstream and closes the issue automatically and atomically.
3384
3385 Otherwise (in case of Rietveld):
3386 Squashes branch into a single commit.
3387 Updates changelog with metadata (e.g. pointer to review).
3388 Pushes/dcommits the code upstream.
3389 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003390 """
3391 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3392 help='bypass upload presubmit hook')
3393 parser.add_option('-m', dest='message',
3394 help="override review description")
3395 parser.add_option('-f', action='store_true', dest='force',
3396 help="force yes to questions (don't prompt)")
3397 parser.add_option('-c', dest='contributor',
3398 help="external contributor for patch (appended to " +
3399 "description and used as author for git). Should be " +
3400 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003401 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003402 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003403 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003404 auth_config = auth.extract_auth_config_from_options(options)
3405
3406 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003407
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003408 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3409 if cl.IsGerrit():
3410 if options.message:
3411 # This could be implemented, but it requires sending a new patch to
3412 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3413 # Besides, Gerrit has the ability to change the commit message on submit
3414 # automatically, thus there is no need to support this option (so far?).
3415 parser.error('-m MESSAGE option is not supported for Gerrit.')
3416 if options.contributor:
3417 parser.error(
3418 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3419 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3420 'the contributor\'s "name <email>". If you can\'t upload such a '
3421 'commit for review, contact your repository admin and request'
3422 '"Forge-Author" permission.')
3423 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3424 options.verbose)
3425
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003426 current = cl.GetBranch()
3427 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3428 if not settings.GetIsGitSvn() and remote == '.':
3429 print
3430 print 'Attempting to push branch %r into another local branch!' % current
3431 print
3432 print 'Either reparent this branch on top of origin/master:'
3433 print ' git reparent-branch --root'
3434 print
3435 print 'OR run `git rebase-update` if you think the parent branch is already'
3436 print 'committed.'
3437 print
3438 print ' Current parent: %r' % upstream_branch
3439 return 1
3440
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003441 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003442 # Default to merging against our best guess of the upstream branch.
3443 args = [cl.GetUpstreamBranch()]
3444
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003445 if options.contributor:
3446 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3447 print "Please provide contibutor as 'First Last <email@example.com>'"
3448 return 1
3449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003450 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003451 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003452
sbc@chromium.org71437c02015-04-09 19:29:40 +00003453 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003454 return 1
3455
3456 # This rev-list syntax means "show all commits not in my branch that
3457 # are in base_branch".
3458 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3459 base_branch]).splitlines()
3460 if upstream_commits:
3461 print ('Base branch "%s" has %d commits '
3462 'not in this branch.' % (base_branch, len(upstream_commits)))
3463 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3464 return 1
3465
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003466 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003467 svn_head = None
3468 if cmd == 'dcommit' or base_has_submodules:
3469 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3470 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003471
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003472 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003473 # If the base_head is a submodule merge commit, the first parent of the
3474 # base_head should be a git-svn commit, which is what we're interested in.
3475 base_svn_head = base_branch
3476 if base_has_submodules:
3477 base_svn_head += '^1'
3478
3479 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003480 if extra_commits:
3481 print ('This branch has %d additional commits not upstreamed yet.'
3482 % len(extra_commits.splitlines()))
3483 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3484 'before attempting to %s.' % (base_branch, cmd))
3485 return 1
3486
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003487 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003488 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003489 author = None
3490 if options.contributor:
3491 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003492 hook_results = cl.RunHook(
3493 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003494 may_prompt=not options.force,
3495 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003496 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003497 if not hook_results.should_continue():
3498 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003499
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003500 # Check the tree status if the tree status URL is set.
3501 status = GetTreeStatus()
3502 if 'closed' == status:
3503 print('The tree is closed. Please wait for it to reopen. Use '
3504 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3505 return 1
3506 elif 'unknown' == status:
3507 print('Unable to determine tree status. Please verify manually and '
3508 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3509 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003510
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003511 change_desc = ChangeDescription(options.message)
3512 if not change_desc.description and cl.GetIssue():
3513 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003514
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003515 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003516 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003517 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003518 else:
3519 print 'No description set.'
3520 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3521 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003522
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003523 # Keep a separate copy for the commit message, because the commit message
3524 # contains the link to the Rietveld issue, while the Rietveld message contains
3525 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003526 # Keep a separate copy for the commit message.
3527 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003528 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003529
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003530 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003531 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003532 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003533 # after it. Add a period on a new line to circumvent this. Also add a space
3534 # before the period to make sure that Gitiles continues to correctly resolve
3535 # the URL.
3536 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003538 commit_desc.append_footer('Patch from %s.' % options.contributor)
3539
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003540 print('Description:')
3541 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003542
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003543 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003544 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003545 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003547 # We want to squash all this branch's commits into one commit with the proper
3548 # description. We do this by doing a "reset --soft" to the base branch (which
3549 # keeps the working copy the same), then dcommitting that. If origin/master
3550 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3551 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003552 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003553 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3554 # Delete the branches if they exist.
3555 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3556 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3557 result = RunGitWithCode(showref_cmd)
3558 if result[0] == 0:
3559 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003560
3561 # We might be in a directory that's present in this branch but not in the
3562 # trunk. Move up to the top of the tree so that git commands that expect a
3563 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003564 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003565 if rel_base_path:
3566 os.chdir(rel_base_path)
3567
3568 # Stuff our change into the merge branch.
3569 # We wrap in a try...finally block so if anything goes wrong,
3570 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003571 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003572 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003573 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003574 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003575 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003576 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003577 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003578 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003579 RunGit(
3580 [
3581 'commit', '--author', options.contributor,
3582 '-m', commit_desc.description,
3583 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003584 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003585 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003586 if base_has_submodules:
3587 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3588 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3589 RunGit(['checkout', CHERRY_PICK_BRANCH])
3590 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003591 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003592 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003593 mirror = settings.GetGitMirror(remote)
3594 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003595 pending_prefix = settings.GetPendingRefPrefix()
3596 if not pending_prefix or branch.startswith(pending_prefix):
3597 # If not using refs/pending/heads/* at all, or target ref is already set
3598 # to pending, then push to the target ref directly.
3599 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003600 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003601 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003602 else:
3603 # Cherry-pick the change on top of pending ref and then push it.
3604 assert branch.startswith('refs/'), branch
3605 assert pending_prefix[-1] == '/', pending_prefix
3606 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003607 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003608 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003609 if retcode == 0:
3610 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003611 else:
3612 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003613 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003614 'svn', 'dcommit',
3615 '-C%s' % options.similarity,
3616 '--no-rebase', '--rmdir',
3617 ]
3618 if settings.GetForceHttpsCommitUrl():
3619 # Allow forcing https commit URLs for some projects that don't allow
3620 # committing to http URLs (like Google Code).
3621 remote_url = cl.GetGitSvnRemoteUrl()
3622 if urlparse.urlparse(remote_url).scheme == 'http':
3623 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003624 cmd_args.append('--commit-url=%s' % remote_url)
3625 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003626 if 'Committed r' in output:
3627 revision = re.match(
3628 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3629 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003630 finally:
3631 # And then swap back to the original branch and clean up.
3632 RunGit(['checkout', '-q', cl.GetBranch()])
3633 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003634 if base_has_submodules:
3635 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003636
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003637 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003638 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003639 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003640
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003641 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003642 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003643 try:
3644 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3645 # We set pushed_to_pending to False, since it made it all the way to the
3646 # real ref.
3647 pushed_to_pending = False
3648 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003649 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003651 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003652 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003653 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003654 if not to_pending:
3655 if viewvc_url and revision:
3656 change_desc.append_footer(
3657 'Committed: %s%s' % (viewvc_url, revision))
3658 elif revision:
3659 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003660 print ('Closing issue '
3661 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003662 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003663 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003664 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003665 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003666 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003667 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003668 if options.bypass_hooks:
3669 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3670 else:
3671 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003672 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003673 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003674
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003675 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003676 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3677 print 'The commit is in the pending queue (%s).' % pending_ref
3678 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003679 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003680 'footer.' % branch)
3681
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003682 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3683 if os.path.isfile(hook):
3684 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003685
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003686 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003687
3688
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003689def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3690 print
3691 print 'Waiting for commit to be landed on %s...' % real_ref
3692 print '(If you are impatient, you may Ctrl-C once without harm)'
3693 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3694 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003695 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003696
3697 loop = 0
3698 while True:
3699 sys.stdout.write('fetching (%d)... \r' % loop)
3700 sys.stdout.flush()
3701 loop += 1
3702
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003703 if mirror:
3704 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003705 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3706 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3707 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3708 for commit in commits.splitlines():
3709 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3710 print 'Found commit on %s' % real_ref
3711 return commit
3712
3713 current_rev = to_rev
3714
3715
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003716def PushToGitPending(remote, pending_ref, upstream_ref):
3717 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3718
3719 Returns:
3720 (retcode of last operation, output log of last operation).
3721 """
3722 assert pending_ref.startswith('refs/'), pending_ref
3723 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3724 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3725 code = 0
3726 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003727 max_attempts = 3
3728 attempts_left = max_attempts
3729 while attempts_left:
3730 if attempts_left != max_attempts:
3731 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3732 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003733
3734 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003735 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003736 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003737 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003738 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003739 print 'Fetch failed with exit code %d.' % code
3740 if out.strip():
3741 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003742 continue
3743
3744 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003745 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003746 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003747 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003748 if code:
3749 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003750 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3751 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003752 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3753 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003754 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003755 return code, out
3756
3757 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003758 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003759 code, out = RunGitWithCode(
3760 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3761 if code == 0:
3762 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003763 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003764 return code, out
3765
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003766 print 'Push failed with exit code %d.' % code
3767 if out.strip():
3768 print out.strip()
3769 if IsFatalPushFailure(out):
3770 print (
3771 'Fatal push error. Make sure your .netrc credentials and git '
3772 'user.email are correct and you have push access to the repo.')
3773 return code, out
3774
3775 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003776 return code, out
3777
3778
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003779def IsFatalPushFailure(push_stdout):
3780 """True if retrying push won't help."""
3781 return '(prohibited by Gerrit)' in push_stdout
3782
3783
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003784@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003785def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003786 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003787 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003788 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003789 # If it looks like previous commits were mirrored with git-svn.
3790 message = """This repository appears to be a git-svn mirror, but no
3791upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3792 else:
3793 message = """This doesn't appear to be an SVN repository.
3794If your project has a true, writeable git repository, you probably want to run
3795'git cl land' instead.
3796If your project has a git mirror of an upstream SVN master, you probably need
3797to run 'git svn init'.
3798
3799Using the wrong command might cause your commit to appear to succeed, and the
3800review to be closed, without actually landing upstream. If you choose to
3801proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003802 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003803 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003804 return SendUpstream(parser, args, 'dcommit')
3805
3806
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003807@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003808def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003809 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003810 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811 print('This appears to be an SVN repository.')
3812 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003813 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003814 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003815 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816
3817
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003818@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003819def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003820 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003821 parser.add_option('-b', dest='newbranch',
3822 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003823 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003824 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003825 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3826 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003827 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003828 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003829 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003830 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003831 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003832 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003833
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003834
3835 group = optparse.OptionGroup(
3836 parser,
3837 'Options for continuing work on the current issue uploaded from a '
3838 'different clone (e.g. different machine). Must be used independently '
3839 'from the other options. No issue number should be specified, and the '
3840 'branch must have an issue number associated with it')
3841 group.add_option('--reapply', action='store_true', dest='reapply',
3842 help='Reset the branch and reapply the issue.\n'
3843 'CAUTION: This will undo any local changes in this '
3844 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003845
3846 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003847 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003848 parser.add_option_group(group)
3849
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003850 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003851 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003852 auth_config = auth.extract_auth_config_from_options(options)
3853
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003854 cl = Changelist(auth_config=auth_config)
3855
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003856 issue_arg = None
3857 if options.reapply :
3858 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003859 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003860
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003861 issue_arg = cl.GetIssue()
3862 upstream = cl.GetUpstreamBranch()
3863 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003864 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003865
3866 RunGit(['reset', '--hard', upstream])
3867 if options.pull:
3868 RunGit(['pull'])
3869 else:
3870 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003871 parser.error('Must specify issue number or url')
3872 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003873
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003874 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003875 parser.print_help()
3876 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003877
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003878 if cl.IsGerrit():
3879 if options.reject:
3880 parser.error('--reject is not supported with Gerrit codereview.')
3881 if options.nocommit:
3882 parser.error('--nocommit is not supported with Gerrit codereview.')
3883 if options.directory:
3884 parser.error('--directory is not supported with Gerrit codereview.')
3885
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003886 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003887 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003888 return 1
3889
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003890 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003891 if options.reapply:
3892 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003893 if options.force:
3894 RunGit(['branch', '-D', options.newbranch],
3895 stderr=subprocess2.PIPE, error_ok=True)
3896 RunGit(['checkout', '-b', options.newbranch,
3897 Changelist().GetUpstreamBranch()])
3898
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003899 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3900 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003901
3902
3903def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003904 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905 # Provide a wrapper for git svn rebase to help avoid accidental
3906 # git svn dcommit.
3907 # It's the only command that doesn't use parser at all since we just defer
3908 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003909
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003910 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003911
3912
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003913def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003914 """Fetches the tree status and returns either 'open', 'closed',
3915 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003916 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003917 if url:
3918 status = urllib2.urlopen(url).read().lower()
3919 if status.find('closed') != -1 or status == '0':
3920 return 'closed'
3921 elif status.find('open') != -1 or status == '1':
3922 return 'open'
3923 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924 return 'unset'
3925
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003927def GetTreeStatusReason():
3928 """Fetches the tree status from a json url and returns the message
3929 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003930 url = settings.GetTreeStatusUrl()
3931 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932 connection = urllib2.urlopen(json_url)
3933 status = json.loads(connection.read())
3934 connection.close()
3935 return status['message']
3936
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003937
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003938def GetBuilderMaster(bot_list):
3939 """For a given builder, fetch the master from AE if available."""
3940 map_url = 'https://builders-map.appspot.com/'
3941 try:
3942 master_map = json.load(urllib2.urlopen(map_url))
3943 except urllib2.URLError as e:
3944 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3945 (map_url, e))
3946 except ValueError as e:
3947 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3948 if not master_map:
3949 return None, 'Failed to build master map.'
3950
3951 result_master = ''
3952 for bot in bot_list:
3953 builder = bot.split(':', 1)[0]
3954 master_list = master_map.get(builder, [])
3955 if not master_list:
3956 return None, ('No matching master for builder %s.' % builder)
3957 elif len(master_list) > 1:
3958 return None, ('The builder name %s exists in multiple masters %s.' %
3959 (builder, master_list))
3960 else:
3961 cur_master = master_list[0]
3962 if not result_master:
3963 result_master = cur_master
3964 elif result_master != cur_master:
3965 return None, 'The builders do not belong to the same master.'
3966 return result_master, None
3967
3968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003969def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003970 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003971 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003972 status = GetTreeStatus()
3973 if 'unset' == status:
3974 print 'You must configure your tree status URL by running "git cl config".'
3975 return 2
3976
3977 print "The tree is %s" % status
3978 print
3979 print GetTreeStatusReason()
3980 if status != 'open':
3981 return 1
3982 return 0
3983
3984
maruel@chromium.org15192402012-09-06 12:38:29 +00003985def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003986 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003987 group = optparse.OptionGroup(parser, "Try job options")
3988 group.add_option(
3989 "-b", "--bot", action="append",
3990 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3991 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003992 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003993 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003994 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003995 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003996 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003997 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003998 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003999 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004000 "-r", "--revision",
4001 help="Revision to use for the try job; default: the "
4002 "revision will be determined by the try server; see "
4003 "its waterfall for more info")
4004 group.add_option(
4005 "-c", "--clobber", action="store_true", default=False,
4006 help="Force a clobber before building; e.g. don't do an "
4007 "incremental build")
4008 group.add_option(
4009 "--project",
4010 help="Override which project to use. Projects are defined "
4011 "server-side to define what default bot set to use")
4012 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004013 "-p", "--property", dest="properties", action="append", default=[],
4014 help="Specify generic properties in the form -p key1=value1 -p "
4015 "key2=value2 etc (buildbucket only). The value will be treated as "
4016 "json if decodable, or as string otherwise.")
4017 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004018 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004019 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004020 "--use-rietveld", action="store_true", default=False,
4021 help="Use Rietveld to trigger try jobs.")
4022 group.add_option(
4023 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4024 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004025 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004026 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004027 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004028 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004029
machenbach@chromium.org45453142015-09-15 08:45:22 +00004030 if options.use_rietveld and options.properties:
4031 parser.error('Properties can only be specified with buildbucket')
4032
4033 # Make sure that all properties are prop=value pairs.
4034 bad_params = [x for x in options.properties if '=' not in x]
4035 if bad_params:
4036 parser.error('Got properties with missing "=": %s' % bad_params)
4037
maruel@chromium.org15192402012-09-06 12:38:29 +00004038 if args:
4039 parser.error('Unknown arguments: %s' % args)
4040
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004041 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004042 if not cl.GetIssue():
4043 parser.error('Need to upload first')
4044
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004045 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004046 if props.get('closed'):
4047 parser.error('Cannot send tryjobs for a closed CL')
4048
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004049 if props.get('private'):
4050 parser.error('Cannot use trybots with private issue')
4051
maruel@chromium.org15192402012-09-06 12:38:29 +00004052 if not options.name:
4053 options.name = cl.GetBranch()
4054
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004055 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004056 options.master, err_msg = GetBuilderMaster(options.bot)
4057 if err_msg:
4058 parser.error('Tryserver master cannot be found because: %s\n'
4059 'Please manually specify the tryserver master'
4060 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004061
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004062 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004063 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004064 if not options.bot:
4065 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004066
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004067 # Get try masters from PRESUBMIT.py files.
4068 masters = presubmit_support.DoGetTryMasters(
4069 change,
4070 change.LocalPaths(),
4071 settings.GetRoot(),
4072 None,
4073 None,
4074 options.verbose,
4075 sys.stdout)
4076 if masters:
4077 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004078
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004079 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4080 options.bot = presubmit_support.DoGetTrySlaves(
4081 change,
4082 change.LocalPaths(),
4083 settings.GetRoot(),
4084 None,
4085 None,
4086 options.verbose,
4087 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004088
4089 if not options.bot:
4090 # Get try masters from cq.cfg if any.
4091 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4092 # location.
4093 cq_cfg = os.path.join(change.RepositoryRoot(),
4094 'infra', 'config', 'cq.cfg')
4095 if os.path.exists(cq_cfg):
4096 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004097 cq_masters = commit_queue.get_master_builder_map(
4098 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004099 for master, builders in cq_masters.iteritems():
4100 for builder in builders:
4101 # Skip presubmit builders, because these will fail without LGTM.
4102 if 'presubmit' not in builder.lower():
4103 masters.setdefault(master, {})[builder] = ['defaulttests']
4104 if masters:
4105 return masters
4106
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004107 if not options.bot:
4108 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004109
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004110 builders_and_tests = {}
4111 # TODO(machenbach): The old style command-line options don't support
4112 # multiple try masters yet.
4113 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4114 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4115
4116 for bot in old_style:
4117 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004118 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004119 elif ',' in bot:
4120 parser.error('Specify one bot per --bot flag')
4121 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004122 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004123
4124 for bot, tests in new_style:
4125 builders_and_tests.setdefault(bot, []).extend(tests)
4126
4127 # Return a master map with one master to be backwards compatible. The
4128 # master name defaults to an empty string, which will cause the master
4129 # not to be set on rietveld (deprecated).
4130 return {options.master: builders_and_tests}
4131
4132 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004133
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004134 for builders in masters.itervalues():
4135 if any('triggered' in b for b in builders):
4136 print >> sys.stderr, (
4137 'ERROR You are trying to send a job to a triggered bot. This type of'
4138 ' bot requires an\ninitial job from a parent (usually a builder). '
4139 'Instead send your job to the parent.\n'
4140 'Bot list: %s' % builders)
4141 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004142
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004143 patchset = cl.GetMostRecentPatchset()
4144 if patchset and patchset != cl.GetPatchset():
4145 print(
4146 '\nWARNING Mismatch between local config and server. Did a previous '
4147 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4148 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004149 if options.luci:
4150 trigger_luci_job(cl, masters, options)
4151 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004152 try:
4153 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4154 except BuildbucketResponseException as ex:
4155 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004156 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004157 except Exception as e:
4158 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4159 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4160 e, stacktrace)
4161 return 1
4162 else:
4163 try:
4164 cl.RpcServer().trigger_distributed_try_jobs(
4165 cl.GetIssue(), patchset, options.name, options.clobber,
4166 options.revision, masters)
4167 except urllib2.HTTPError as e:
4168 if e.code == 404:
4169 print('404 from rietveld; '
4170 'did you mean to use "git try" instead of "git cl try"?')
4171 return 1
4172 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004173
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004174 for (master, builders) in sorted(masters.iteritems()):
4175 if master:
4176 print 'Master: %s' % master
4177 length = max(len(builder) for builder in builders)
4178 for builder in sorted(builders):
4179 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004180 return 0
4181
4182
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004183def CMDtry_results(parser, args):
4184 group = optparse.OptionGroup(parser, "Try job results options")
4185 group.add_option(
4186 "-p", "--patchset", type=int, help="patchset number if not current.")
4187 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004188 "--print-master", action='store_true', help="print master name as well.")
4189 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004190 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004191 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004192 group.add_option(
4193 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4194 help="Host of buildbucket. The default host is %default.")
4195 parser.add_option_group(group)
4196 auth.add_auth_options(parser)
4197 options, args = parser.parse_args(args)
4198 if args:
4199 parser.error('Unrecognized args: %s' % ' '.join(args))
4200
4201 auth_config = auth.extract_auth_config_from_options(options)
4202 cl = Changelist(auth_config=auth_config)
4203 if not cl.GetIssue():
4204 parser.error('Need to upload first')
4205
4206 if not options.patchset:
4207 options.patchset = cl.GetMostRecentPatchset()
4208 if options.patchset and options.patchset != cl.GetPatchset():
4209 print(
4210 '\nWARNING Mismatch between local config and server. Did a previous '
4211 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4212 'Continuing using\npatchset %s.\n' % options.patchset)
4213 try:
4214 jobs = fetch_try_jobs(auth_config, cl, options)
4215 except BuildbucketResponseException as ex:
4216 print 'Buildbucket error: %s' % ex
4217 return 1
4218 except Exception as e:
4219 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4220 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4221 e, stacktrace)
4222 return 1
4223 print_tryjobs(options, jobs)
4224 return 0
4225
4226
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004227@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004229 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004230 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004231 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004232 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004234 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004235 if args:
4236 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004237 branch = cl.GetBranch()
4238 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004239 cl = Changelist()
4240 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004241
4242 # Clear configured merge-base, if there is one.
4243 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004244 else:
4245 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246 return 0
4247
4248
thestig@chromium.org00858c82013-12-02 23:08:03 +00004249def CMDweb(parser, args):
4250 """Opens the current CL in the web browser."""
4251 _, args = parser.parse_args(args)
4252 if args:
4253 parser.error('Unrecognized args: %s' % ' '.join(args))
4254
4255 issue_url = Changelist().GetIssueURL()
4256 if not issue_url:
4257 print >> sys.stderr, 'ERROR No issue to open'
4258 return 1
4259
4260 webbrowser.open(issue_url)
4261 return 0
4262
4263
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004264def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004265 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004266 auth.add_auth_options(parser)
4267 options, args = parser.parse_args(args)
4268 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004269 if args:
4270 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004271 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004272 props = cl.GetIssueProperties()
4273 if props.get('private'):
4274 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004275 cl.SetFlag('commit', '1')
4276 return 0
4277
4278
groby@chromium.org411034a2013-02-26 15:12:01 +00004279def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004280 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004281 auth.add_auth_options(parser)
4282 options, args = parser.parse_args(args)
4283 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004284 if args:
4285 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004286 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004287 # Ensure there actually is an issue to close.
4288 cl.GetDescription()
4289 cl.CloseIssue()
4290 return 0
4291
4292
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004293def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004294 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004295 auth.add_auth_options(parser)
4296 options, args = parser.parse_args(args)
4297 auth_config = auth.extract_auth_config_from_options(options)
4298 if args:
4299 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004300
4301 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004302 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004303 # Staged changes would be committed along with the patch from last
4304 # upload, hence counted toward the "last upload" side in the final
4305 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004306 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004307 return 1
4308
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004309 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004310 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004311 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004312 if not issue:
4313 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004314 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004315 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004316
4317 # Create a new branch based on the merge-base
4318 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
4319 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004320 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004321 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004322 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004323 return rtn
4324
wychen@chromium.org06928532015-02-03 02:11:29 +00004325 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004326 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004327 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004328 finally:
4329 RunGit(['checkout', '-q', branch])
4330 RunGit(['branch', '-D', TMP_BRANCH])
4331
4332 return 0
4333
4334
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004335def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004336 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004337 parser.add_option(
4338 '--no-color',
4339 action='store_true',
4340 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004341 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004342 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004343 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004344
4345 author = RunGit(['config', 'user.email']).strip() or None
4346
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004347 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004348
4349 if args:
4350 if len(args) > 1:
4351 parser.error('Unknown args')
4352 base_branch = args[0]
4353 else:
4354 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004355 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004356
4357 change = cl.GetChange(base_branch, None)
4358 return owners_finder.OwnersFinder(
4359 [f.LocalPath() for f in
4360 cl.GetChange(base_branch, None).AffectedFiles()],
4361 change.RepositoryRoot(), author,
4362 fopen=file, os_path=os.path, glob=glob.glob,
4363 disable_color=options.no_color).run()
4364
4365
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004366def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004367 """Generates a diff command."""
4368 # Generate diff for the current branch's changes.
4369 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4370 upstream_commit, '--' ]
4371
4372 if args:
4373 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004374 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004375 diff_cmd.append(arg)
4376 else:
4377 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004378
4379 return diff_cmd
4380
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004381def MatchingFileType(file_name, extensions):
4382 """Returns true if the file name ends with one of the given extensions."""
4383 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004384
enne@chromium.org555cfe42014-01-29 18:21:39 +00004385@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004386def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004387 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004388 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004389 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004390 parser.add_option('--full', action='store_true',
4391 help='Reformat the full content of all touched files')
4392 parser.add_option('--dry-run', action='store_true',
4393 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004394 parser.add_option('--python', action='store_true',
4395 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004396 parser.add_option('--diff', action='store_true',
4397 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004398 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004399
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004400 # git diff generates paths against the root of the repository. Change
4401 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004402 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004403 if rel_base_path:
4404 os.chdir(rel_base_path)
4405
digit@chromium.org29e47272013-05-17 17:01:46 +00004406 # Grab the merge-base commit, i.e. the upstream commit of the current
4407 # branch when it was created or the last time it was rebased. This is
4408 # to cover the case where the user may have called "git fetch origin",
4409 # moving the origin branch to a newer commit, but hasn't rebased yet.
4410 upstream_commit = None
4411 cl = Changelist()
4412 upstream_branch = cl.GetUpstreamBranch()
4413 if upstream_branch:
4414 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4415 upstream_commit = upstream_commit.strip()
4416
4417 if not upstream_commit:
4418 DieWithError('Could not find base commit for this branch. '
4419 'Are you in detached state?')
4420
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004421 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4422 diff_output = RunGit(changed_files_cmd)
4423 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004424 # Filter out files deleted by this CL
4425 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004426
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004427 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4428 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4429 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004430 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004431
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004432 top_dir = os.path.normpath(
4433 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4434
4435 # Locate the clang-format binary in the checkout
4436 try:
4437 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4438 except clang_format.NotFoundError, e:
4439 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004440
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004441 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4442 # formatted. This is used to block during the presubmit.
4443 return_value = 0
4444
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004445 if clang_diff_files:
4446 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004447 cmd = [clang_format_tool]
4448 if not opts.dry_run and not opts.diff:
4449 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004450 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004451 if opts.diff:
4452 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004453 else:
4454 env = os.environ.copy()
4455 env['PATH'] = str(os.path.dirname(clang_format_tool))
4456 try:
4457 script = clang_format.FindClangFormatScriptInChromiumTree(
4458 'clang-format-diff.py')
4459 except clang_format.NotFoundError, e:
4460 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004461
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004462 cmd = [sys.executable, script, '-p0']
4463 if not opts.dry_run and not opts.diff:
4464 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004465
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004466 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4467 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004468
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004469 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4470 if opts.diff:
4471 sys.stdout.write(stdout)
4472 if opts.dry_run and len(stdout) > 0:
4473 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004474
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004475 # Similar code to above, but using yapf on .py files rather than clang-format
4476 # on C/C++ files
4477 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004478 yapf_tool = gclient_utils.FindExecutable('yapf')
4479 if yapf_tool is None:
4480 DieWithError('yapf not found in PATH')
4481
4482 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004483 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004484 cmd = [yapf_tool]
4485 if not opts.dry_run and not opts.diff:
4486 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004487 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004488 if opts.diff:
4489 sys.stdout.write(stdout)
4490 else:
4491 # TODO(sbc): yapf --lines mode still has some issues.
4492 # https://github.com/google/yapf/issues/154
4493 DieWithError('--python currently only works with --full')
4494
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004495 # Dart's formatter does not have the nice property of only operating on
4496 # modified chunks, so hard code full.
4497 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004498 try:
4499 command = [dart_format.FindDartFmtToolInChromiumTree()]
4500 if not opts.dry_run and not opts.diff:
4501 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004502 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004503
ppi@chromium.org6593d932016-03-03 15:41:15 +00004504 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004505 if opts.dry_run and stdout:
4506 return_value = 2
4507 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004508 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4509 'found in this checkout. Files in other languages are still ' +
4510 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004511
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004512 # Format GN build files. Always run on full build files for canonical form.
4513 if gn_diff_files:
4514 cmd = ['gn', 'format']
4515 if not opts.dry_run and not opts.diff:
4516 cmd.append('--in-place')
4517 for gn_diff_file in gn_diff_files:
4518 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4519 if opts.diff:
4520 sys.stdout.write(stdout)
4521
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004522 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004523
4524
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004525@subcommand.usage('<codereview url or issue id>')
4526def CMDcheckout(parser, args):
4527 """Checks out a branch associated with a given Rietveld issue."""
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004528 # TODO(tandrii): consider adding this for Gerrit?
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004529 _, args = parser.parse_args(args)
4530
4531 if len(args) != 1:
4532 parser.print_help()
4533 return 1
4534
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004535 issue_arg = ParseIssueNumberArgument(args[0])
4536 if issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004537 parser.print_help()
4538 return 1
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004539 target_issue = issue_arg.issue
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004540
4541 key_and_issues = [x.split() for x in RunGit(
4542 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4543 .splitlines()]
4544 branches = []
4545 for key, issue in key_and_issues:
4546 if issue == target_issue:
4547 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4548
4549 if len(branches) == 0:
4550 print 'No branch found for issue %s.' % target_issue
4551 return 1
4552 if len(branches) == 1:
4553 RunGit(['checkout', branches[0]])
4554 else:
4555 print 'Multiple branches match issue %s:' % target_issue
4556 for i in range(len(branches)):
4557 print '%d: %s' % (i, branches[i])
4558 which = raw_input('Choose by index: ')
4559 try:
4560 RunGit(['checkout', branches[int(which)]])
4561 except (IndexError, ValueError):
4562 print 'Invalid selection, not checking out any branch.'
4563 return 1
4564
4565 return 0
4566
4567
maruel@chromium.org29404b52014-09-08 22:58:00 +00004568def CMDlol(parser, args):
4569 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004570 print zlib.decompress(base64.b64decode(
4571 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4572 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4573 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4574 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004575 return 0
4576
4577
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004578class OptionParser(optparse.OptionParser):
4579 """Creates the option parse and add --verbose support."""
4580 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004581 optparse.OptionParser.__init__(
4582 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004583 self.add_option(
4584 '-v', '--verbose', action='count', default=0,
4585 help='Use 2 times for more debugging info')
4586
4587 def parse_args(self, args=None, values=None):
4588 options, args = optparse.OptionParser.parse_args(self, args, values)
4589 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4590 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4591 return options, args
4592
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004593
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004594def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004595 if sys.hexversion < 0x02060000:
4596 print >> sys.stderr, (
4597 '\nYour python version %s is unsupported, please upgrade.\n' %
4598 sys.version.split(' ', 1)[0])
4599 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004600
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004601 # Reload settings.
4602 global settings
4603 settings = Settings()
4604
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004605 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004606 dispatcher = subcommand.CommandDispatcher(__name__)
4607 try:
4608 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004609 except auth.AuthenticationError as e:
4610 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004611 except urllib2.HTTPError, e:
4612 if e.code != 500:
4613 raise
4614 DieWithError(
4615 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4616 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004617 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004618
4619
4620if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004621 # These affect sys.stdout so do it outside of main() to simplify mocks in
4622 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004623 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004624 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004625 try:
4626 sys.exit(main(sys.argv[1:]))
4627 except KeyboardInterrupt:
4628 sys.stderr.write('interrupted\n')
4629 sys.exit(1)