blob: 4c6bb10ce6f356b2a94d2f1c912198ca13c1ecf9 [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
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
maruel@chromium.org0633fb42013-08-16 20:06:14 +000063__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000065DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000066POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000068GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000078# Shortcut since it quickly becomes redundant.
79Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000080
maruel@chromium.orgddd59412011-11-30 14:20:38 +000081# Initialized in main()
82settings = None
83
84
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000086 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 sys.exit(1)
88
89
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000090def GetNoGitPagerEnv():
91 env = os.environ.copy()
92 # 'cat' is a magical git string that disables pagers on all platforms.
93 env['GIT_PAGER'] = 'cat'
94 return env
95
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000096
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000099 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000100 except subprocess2.CalledProcessError as e:
101 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 'Command "%s" failed.\n%s' % (
105 ' '.join(args), error_message or e.stdout or ''))
106 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107
108
109def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000110 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000111 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000115 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000116 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000121 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000122 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000123 stdout=subprocess2.PIPE,
124 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000125 return code, out[0]
126 except ValueError:
127 # When the subprocess fails, it returns None. That triggers a ValueError
128 # when trying to unpack the return value into (out, code).
129 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
133 """Returns stdout, suppresses stderr and ingores the return code."""
134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000160 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000161 if not branch:
162 return
163
164 cmd = ['config']
165 if isinstance(value, int):
166 cmd.append('--int')
167 git_key = 'branch.%s.%s' % (branch, key)
168 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169
170
171def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000172 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000173 if branch:
174 git_key = 'branch.%s.%s' % (branch, key)
175 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
176 try:
177 return int(stdout.strip())
178 except ValueError:
179 pass
180 return default
181
182
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183def add_git_similarity(parser):
184 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186 help='Sets the percentage that a pair of files need to match in order to'
187 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 parser.add_option(
189 '--find-copies', action='store_true',
190 help='Allows git to look for copies.')
191 parser.add_option(
192 '--no-find-copies', action='store_false', dest='find_copies',
193 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000194
195 old_parser_args = parser.parse_args
196 def Parse(args):
197 options, args = old_parser_args(args)
198
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 print('Note: Saving similarity of %d%% in git config.'
203 % options.similarity)
204 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000205
iannucci@chromium.org79540052012-10-19 23:15:26 +0000206 options.similarity = max(0, min(options.similarity, 100))
207
208 if options.find_copies is None:
209 options.find_copies = bool(
210 git_get_branch_default('git-find-copies', True))
211 else:
212 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000213
214 print('Using %d%% similarity for rename/copy detection. '
215 'Override with --similarity.' % options.similarity)
216
217 return options, args
218 parser.parse_args = Parse
219
220
machenbach@chromium.org45453142015-09-15 08:45:22 +0000221def _get_properties_from_options(options):
222 properties = dict(x.split('=', 1) for x in options.properties)
223 for key, val in properties.iteritems():
224 try:
225 properties[key] = json.loads(val)
226 except ValueError:
227 pass # If a value couldn't be evaluated, treat it as a string.
228 return properties
229
230
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000231def _prefix_master(master):
232 """Convert user-specified master name to full master name.
233
234 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
235 name, while the developers always use shortened master name
236 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
237 function does the conversion for buildbucket migration.
238 """
239 prefix = 'master.'
240 if master.startswith(prefix):
241 return master
242 return '%s%s' % (prefix, master)
243
244
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000245def _buildbucket_retry(operation_name, http, *args, **kwargs):
246 """Retries requests to buildbucket service and returns parsed json content."""
247 try_count = 0
248 while True:
249 response, content = http.request(*args, **kwargs)
250 try:
251 content_json = json.loads(content)
252 except ValueError:
253 content_json = None
254
255 # Buildbucket could return an error even if status==200.
256 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000257 error = content_json.get('error')
258 if error.get('code') == 403:
259 raise BuildbucketResponseException(
260 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000261 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000262 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 raise BuildbucketResponseException(msg)
264
265 if response.status == 200:
266 if not content_json:
267 raise BuildbucketResponseException(
268 'Buildbucket returns invalid json content: %s.\n'
269 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
270 content)
271 return content_json
272 if response.status < 500 or try_count >= 2:
273 raise httplib2.HttpLib2Error(content)
274
275 # status >= 500 means transient failures.
276 logging.debug('Transient errors when %s. Will retry.', operation_name)
277 time.sleep(0.5 + 1.5*try_count)
278 try_count += 1
279 assert False, 'unreachable'
280
281
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000282def trigger_luci_job(changelist, masters, options):
283 """Send a job to run on LUCI."""
284 issue_props = changelist.GetIssueProperties()
285 issue = changelist.GetIssue()
286 patchset = changelist.GetMostRecentPatchset()
287 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000288 # TODO(hinoka et al): add support for other properties.
289 # Currently, this completely ignores testfilter and other properties.
290 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000291 luci_trigger.trigger(
292 builder, 'HEAD', issue, patchset, issue_props['project'])
293
294
machenbach@chromium.org45453142015-09-15 08:45:22 +0000295def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000296 rietveld_url = settings.GetDefaultServerUrl()
297 rietveld_host = urlparse.urlparse(rietveld_url).hostname
298 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
299 http = authenticator.authorize(httplib2.Http())
300 http.force_exception_to_status_code = True
301 issue_props = changelist.GetIssueProperties()
302 issue = changelist.GetIssue()
303 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305
306 buildbucket_put_url = (
307 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000308 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000309 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
310 hostname=rietveld_host,
311 issue=issue,
312 patch=patchset)
313
314 batch_req_body = {'builds': []}
315 print_text = []
316 print_text.append('Tried jobs on:')
317 for master, builders_and_tests in sorted(masters.iteritems()):
318 print_text.append('Master: %s' % master)
319 bucket = _prefix_master(master)
320 for builder, tests in sorted(builders_and_tests.iteritems()):
321 print_text.append(' %s: %s' % (builder, tests))
322 parameters = {
323 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000324 'changes': [{
325 'author': {'email': issue_props['owner_email']},
326 'revision': options.revision,
327 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000328 'properties': {
329 'category': category,
330 'issue': issue,
331 'master': master,
332 'patch_project': issue_props['project'],
333 'patch_storage': 'rietveld',
334 'patchset': patchset,
335 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 },
338 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000339 if tests:
340 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000341 if properties:
342 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000343 if options.clobber:
344 parameters['properties']['clobber'] = True
345 batch_req_body['builds'].append(
346 {
347 'bucket': bucket,
348 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000350 'tags': ['builder:%s' % builder,
351 'buildset:%s' % buildset,
352 'master:%s' % master,
353 'user_agent:git_cl_try']
354 }
355 )
356
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000357 _buildbucket_retry(
358 'triggering tryjobs',
359 http,
360 buildbucket_put_url,
361 'PUT',
362 body=json.dumps(batch_req_body),
363 headers={'Content-Type': 'application/json'}
364 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000365 print_text.append('To see results here, run: git cl try-results')
366 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000367 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000368
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370def fetch_try_jobs(auth_config, changelist, options):
371 """Fetches tryjobs from buildbucket.
372
373 Returns a map from build id to build info as json dictionary.
374 """
375 rietveld_url = settings.GetDefaultServerUrl()
376 rietveld_host = urlparse.urlparse(rietveld_url).hostname
377 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
378 if authenticator.has_cached_credentials():
379 http = authenticator.authorize(httplib2.Http())
380 else:
381 print ('Warning: Some results might be missing because %s' %
382 # Get the message on how to login.
383 auth.LoginRequiredError(rietveld_host).message)
384 http = httplib2.Http()
385
386 http.force_exception_to_status_code = True
387
388 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
389 hostname=rietveld_host,
390 issue=changelist.GetIssue(),
391 patch=options.patchset)
392 params = {'tag': 'buildset:%s' % buildset}
393
394 builds = {}
395 while True:
396 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
397 hostname=options.buildbucket_host,
398 params=urllib.urlencode(params))
399 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
400 for build in content.get('builds', []):
401 builds[build['id']] = build
402 if 'next_cursor' in content:
403 params['start_cursor'] = content['next_cursor']
404 else:
405 break
406 return builds
407
408
409def print_tryjobs(options, builds):
410 """Prints nicely result of fetch_try_jobs."""
411 if not builds:
412 print 'No tryjobs scheduled'
413 return
414
415 # Make a copy, because we'll be modifying builds dictionary.
416 builds = builds.copy()
417 builder_names_cache = {}
418
419 def get_builder(b):
420 try:
421 return builder_names_cache[b['id']]
422 except KeyError:
423 try:
424 parameters = json.loads(b['parameters_json'])
425 name = parameters['builder_name']
426 except (ValueError, KeyError) as error:
427 print 'WARNING: failed to get builder name for build %s: %s' % (
428 b['id'], error)
429 name = None
430 builder_names_cache[b['id']] = name
431 return name
432
433 def get_bucket(b):
434 bucket = b['bucket']
435 if bucket.startswith('master.'):
436 return bucket[len('master.'):]
437 return bucket
438
439 if options.print_master:
440 name_fmt = '%%-%ds %%-%ds' % (
441 max(len(str(get_bucket(b))) for b in builds.itervalues()),
442 max(len(str(get_builder(b))) for b in builds.itervalues()))
443 def get_name(b):
444 return name_fmt % (get_bucket(b), get_builder(b))
445 else:
446 name_fmt = '%%-%ds' % (
447 max(len(str(get_builder(b))) for b in builds.itervalues()))
448 def get_name(b):
449 return name_fmt % get_builder(b)
450
451 def sort_key(b):
452 return b['status'], b.get('result'), get_name(b), b.get('url')
453
454 def pop(title, f, color=None, **kwargs):
455 """Pop matching builds from `builds` dict and print them."""
456
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000457 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000458 colorize = str
459 else:
460 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
461
462 result = []
463 for b in builds.values():
464 if all(b.get(k) == v for k, v in kwargs.iteritems()):
465 builds.pop(b['id'])
466 result.append(b)
467 if result:
468 print colorize(title)
469 for b in sorted(result, key=sort_key):
470 print ' ', colorize('\t'.join(map(str, f(b))))
471
472 total = len(builds)
473 pop(status='COMPLETED', result='SUCCESS',
474 title='Successes:', color=Fore.GREEN,
475 f=lambda b: (get_name(b), b.get('url')))
476 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
477 title='Infra Failures:', color=Fore.MAGENTA,
478 f=lambda b: (get_name(b), b.get('url')))
479 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
480 title='Failures:', color=Fore.RED,
481 f=lambda b: (get_name(b), b.get('url')))
482 pop(status='COMPLETED', result='CANCELED',
483 title='Canceled:', color=Fore.MAGENTA,
484 f=lambda b: (get_name(b),))
485 pop(status='COMPLETED', result='FAILURE',
486 failure_reason='INVALID_BUILD_DEFINITION',
487 title='Wrong master/builder name:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 title='Other failures:',
491 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
492 pop(status='COMPLETED',
493 title='Other finished:',
494 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
495 pop(status='STARTED',
496 title='Started:', color=Fore.YELLOW,
497 f=lambda b: (get_name(b), b.get('url')))
498 pop(status='SCHEDULED',
499 title='Scheduled:',
500 f=lambda b: (get_name(b), 'id=%s' % b['id']))
501 # The last section is just in case buildbucket API changes OR there is a bug.
502 pop(title='Other:',
503 f=lambda b: (get_name(b), 'id=%s' % b['id']))
504 assert len(builds) == 0
505 print 'Total: %d tryjobs' % total
506
507
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000508def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
509 """Return the corresponding git ref if |base_url| together with |glob_spec|
510 matches the full |url|.
511
512 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
513 """
514 fetch_suburl, as_ref = glob_spec.split(':')
515 if allow_wildcards:
516 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
517 if glob_match:
518 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
519 # "branches/{472,597,648}/src:refs/remotes/svn/*".
520 branch_re = re.escape(base_url)
521 if glob_match.group(1):
522 branch_re += '/' + re.escape(glob_match.group(1))
523 wildcard = glob_match.group(2)
524 if wildcard == '*':
525 branch_re += '([^/]*)'
526 else:
527 # Escape and replace surrounding braces with parentheses and commas
528 # with pipe symbols.
529 wildcard = re.escape(wildcard)
530 wildcard = re.sub('^\\\\{', '(', wildcard)
531 wildcard = re.sub('\\\\,', '|', wildcard)
532 wildcard = re.sub('\\\\}$', ')', wildcard)
533 branch_re += wildcard
534 if glob_match.group(3):
535 branch_re += re.escape(glob_match.group(3))
536 match = re.match(branch_re, url)
537 if match:
538 return re.sub('\*$', match.group(1), as_ref)
539
540 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
541 if fetch_suburl:
542 full_url = base_url + '/' + fetch_suburl
543 else:
544 full_url = base_url
545 if full_url == url:
546 return as_ref
547 return None
548
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000549
iannucci@chromium.org79540052012-10-19 23:15:26 +0000550def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000551 """Prints statistics about the change to the user."""
552 # --no-ext-diff is broken in some versions of Git, so try to work around
553 # this by overriding the environment (but there is still a problem if the
554 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000555 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000556 if 'GIT_EXTERNAL_DIFF' in env:
557 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000558
559 if find_copies:
560 similarity_options = ['--find-copies-harder', '-l100000',
561 '-C%s' % similarity]
562 else:
563 similarity_options = ['-M%s' % similarity]
564
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000565 try:
566 stdout = sys.stdout.fileno()
567 except AttributeError:
568 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000569 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000570 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000571 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000572 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573
574
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000575class BuildbucketResponseException(Exception):
576 pass
577
578
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579class Settings(object):
580 def __init__(self):
581 self.default_server = None
582 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000583 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584 self.is_git_svn = None
585 self.svn_branch = None
586 self.tree_status_url = None
587 self.viewvc_url = None
588 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000589 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000590 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000591 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000592 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000593 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000594 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000595
596 def LazyUpdateIfNeeded(self):
597 """Updates the settings from a codereview.settings file, if available."""
598 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000599 # The only value that actually changes the behavior is
600 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000601 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 error_ok=True
603 ).strip().lower()
604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000606 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 LoadCodereviewSettingsFromFile(cr_settings_file)
608 self.updated = True
609
610 def GetDefaultServerUrl(self, error_ok=False):
611 if not self.default_server:
612 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000614 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 if error_ok:
616 return self.default_server
617 if not self.default_server:
618 error_message = ('Could not find settings file. You must configure '
619 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000621 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.default_server
623
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000624 @staticmethod
625 def GetRelativeRoot():
626 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 if self.root is None:
630 self.root = os.path.abspath(self.GetRelativeRoot())
631 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000633 def GetGitMirror(self, remote='origin'):
634 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000635 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000636 if not os.path.isdir(local_url):
637 return None
638 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
639 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
640 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
641 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
642 if mirror.exists():
643 return mirror
644 return None
645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 def GetIsGitSvn(self):
647 """Return true if this repo looks like it's using git-svn."""
648 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000649 if self.GetPendingRefPrefix():
650 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
651 self.is_git_svn = False
652 else:
653 # If you have any "svn-remote.*" config keys, we think you're using svn.
654 self.is_git_svn = RunGitWithCode(
655 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 return self.is_git_svn
657
658 def GetSVNBranch(self):
659 if self.svn_branch is None:
660 if not self.GetIsGitSvn():
661 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
662
663 # Try to figure out which remote branch we're based on.
664 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000665 # 1) iterate through our branch history and find the svn URL.
666 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667
668 # regexp matching the git-svn line that contains the URL.
669 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
670
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 # We don't want to go through all of history, so read a line from the
672 # pipe at a time.
673 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000674 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000675 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
676 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000677 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000678 for line in proc.stdout:
679 match = git_svn_re.match(line)
680 if match:
681 url = match.group(1)
682 proc.stdout.close() # Cut pipe.
683 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000685 if url:
686 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
687 remotes = RunGit(['config', '--get-regexp',
688 r'^svn-remote\..*\.url']).splitlines()
689 for remote in remotes:
690 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000692 remote = match.group(1)
693 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000694 rewrite_root = RunGit(
695 ['config', 'svn-remote.%s.rewriteRoot' % remote],
696 error_ok=True).strip()
697 if rewrite_root:
698 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000699 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000700 ['config', 'svn-remote.%s.fetch' % remote],
701 error_ok=True).strip()
702 if fetch_spec:
703 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
704 if self.svn_branch:
705 break
706 branch_spec = RunGit(
707 ['config', 'svn-remote.%s.branches' % remote],
708 error_ok=True).strip()
709 if branch_spec:
710 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
711 if self.svn_branch:
712 break
713 tag_spec = RunGit(
714 ['config', 'svn-remote.%s.tags' % remote],
715 error_ok=True).strip()
716 if tag_spec:
717 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
718 if self.svn_branch:
719 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720
721 if not self.svn_branch:
722 DieWithError('Can\'t guess svn branch -- try specifying it on the '
723 'command line')
724
725 return self.svn_branch
726
727 def GetTreeStatusUrl(self, error_ok=False):
728 if not self.tree_status_url:
729 error_message = ('You must configure your tree status URL by running '
730 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000731 self.tree_status_url = self._GetRietveldConfig(
732 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 return self.tree_status_url
734
735 def GetViewVCUrl(self):
736 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.viewvc_url
739
rmistry@google.com90752582014-01-14 21:04:50 +0000740 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000742
rmistry@google.com78948ed2015-07-08 23:09:57 +0000743 def GetIsSkipDependencyUpload(self, branch_name):
744 """Returns true if specified branch should skip dep uploads."""
745 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
746 error_ok=True)
747
rmistry@google.com5626a922015-02-26 14:03:30 +0000748 def GetRunPostUploadHook(self):
749 run_post_upload_hook = self._GetRietveldConfig(
750 'run-post-upload-hook', error_ok=True)
751 return run_post_upload_hook == "True"
752
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000753 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000754 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000755
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000756 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000758
ukai@chromium.orge8077812012-02-03 03:41:46 +0000759 def GetIsGerrit(self):
760 """Return true if this repo is assosiated with gerrit code review system."""
761 if self.is_gerrit is None:
762 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
763 return self.is_gerrit
764
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000765 def GetSquashGerritUploads(self):
766 """Return true if uploads to Gerrit should be squashed by default."""
767 if self.squash_gerrit_uploads is None:
768 self.squash_gerrit_uploads = (
769 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
770 error_ok=True).strip() == 'true')
771 return self.squash_gerrit_uploads
772
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000773 def GetGitEditor(self):
774 """Return the editor specified in the git config, or None if none is."""
775 if self.git_editor is None:
776 self.git_editor = self._GetConfig('core.editor', error_ok=True)
777 return self.git_editor or None
778
thestig@chromium.org44202a22014-03-11 19:22:18 +0000779 def GetLintRegex(self):
780 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
781 DEFAULT_LINT_REGEX)
782
783 def GetLintIgnoreRegex(self):
784 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
785 DEFAULT_LINT_IGNORE_REGEX)
786
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000787 def GetProject(self):
788 if not self.project:
789 self.project = self._GetRietveldConfig('project', error_ok=True)
790 return self.project
791
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000792 def GetForceHttpsCommitUrl(self):
793 if not self.force_https_commit_url:
794 self.force_https_commit_url = self._GetRietveldConfig(
795 'force-https-commit-url', error_ok=True)
796 return self.force_https_commit_url
797
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000798 def GetPendingRefPrefix(self):
799 if not self.pending_ref_prefix:
800 self.pending_ref_prefix = self._GetRietveldConfig(
801 'pending-ref-prefix', error_ok=True)
802 return self.pending_ref_prefix
803
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 def _GetRietveldConfig(self, param, **kwargs):
805 return self._GetConfig('rietveld.' + param, **kwargs)
806
rmistry@google.com78948ed2015-07-08 23:09:57 +0000807 def _GetBranchConfig(self, branch_name, param, **kwargs):
808 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 def _GetConfig(self, param, **kwargs):
811 self.LazyUpdateIfNeeded()
812 return RunGit(['config', param], **kwargs).strip()
813
814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815def ShortBranchName(branch):
816 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000817 return branch.replace('refs/heads/', '', 1)
818
819
820def GetCurrentBranchRef():
821 """Returns branch ref (e.g., refs/heads/master) or None."""
822 return RunGit(['symbolic-ref', 'HEAD'],
823 stderr=subprocess2.VOID, error_ok=True).strip() or None
824
825
826def GetCurrentBranch():
827 """Returns current branch or None.
828
829 For refs/heads/* branches, returns just last part. For others, full ref.
830 """
831 branchref = GetCurrentBranchRef()
832 if branchref:
833 return ShortBranchName(branchref)
834 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
836
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +0000837class _ParsedIssueNumberArgument(object):
838 def __init__(self, issue=None, patchset=None, hostname=None):
839 self.issue = issue
840 self.patchset = patchset
841 self.hostname = hostname
842
843 @property
844 def valid(self):
845 return self.issue is not None
846
847
848class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
849 def __init__(self, *args, **kwargs):
850 self.patch_url = kwargs.pop('patch_url', None)
851 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
852
853
854def ParseIssueNumberArgument(arg):
855 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
856 fail_result = _ParsedIssueNumberArgument()
857
858 if arg.isdigit():
859 return _ParsedIssueNumberArgument(issue=int(arg))
860 if not arg.startswith('http'):
861 return fail_result
862 url = gclient_utils.UpgradeToHttps(arg)
863 try:
864 parsed_url = urlparse.urlparse(url)
865 except ValueError:
866 return fail_result
867 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
868 tmp = cls.ParseIssueURL(parsed_url)
869 if tmp is not None:
870 return tmp
871 return fail_result
872
873
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000875 """Changelist works with one changelist in local branch.
876
877 Supports two codereview backends: Rietveld or Gerrit, selected at object
878 creation.
879
880 Not safe for concurrent multi-{thread,process} use.
881 """
882
883 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
884 """Create a new ChangeList instance.
885
886 If issue is given, the codereview must be given too.
887
888 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
889 Otherwise, it's decided based on current configuration of the local branch,
890 with default being 'rietveld' for backwards compatibility.
891 See _load_codereview_impl for more details.
892
893 **kwargs will be passed directly to codereview implementation.
894 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000896 global settings
897 if not settings:
898 # Happens when git_cl.py is used as a utility library.
899 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000900
901 if issue:
902 assert codereview, 'codereview must be known, if issue is known'
903
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904 self.branchref = branchref
905 if self.branchref:
906 self.branch = ShortBranchName(self.branchref)
907 else:
908 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000910 self.lookedup_issue = False
911 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912 self.has_description = False
913 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000914 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000916 self.cc = None
917 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000918 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000920 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000921 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000922 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000923 assert self._codereview_impl
924 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000925
926 def _load_codereview_impl(self, codereview=None, **kwargs):
927 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000928 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
929 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
930 self._codereview = codereview
931 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000932 return
933
934 # Automatic selection based on issue number set for a current branch.
935 # Rietveld takes precedence over Gerrit.
936 assert not self.issue
937 # Whether we find issue or not, we are doing the lookup.
938 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000939 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000940 setting = cls.IssueSetting(self.GetBranch())
941 issue = RunGit(['config', setting], error_ok=True).strip()
942 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000943 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000944 self._codereview_impl = cls(self, **kwargs)
945 self.issue = int(issue)
946 return
947
948 # No issue is set for this branch, so decide based on repo-wide settings.
949 return self._load_codereview_impl(
950 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
951 **kwargs)
952
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000953 def IsGerrit(self):
954 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000955
956 def GetCCList(self):
957 """Return the users cc'd on this CL.
958
959 Return is a string suitable for passing to gcl with the --cc flag.
960 """
961 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000962 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000963 more_cc = ','.join(self.watchers)
964 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
965 return self.cc
966
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000967 def GetCCListWithoutDefault(self):
968 """Return the users cc'd on this CL excluding default ones."""
969 if self.cc is None:
970 self.cc = ','.join(self.watchers)
971 return self.cc
972
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000973 def SetWatchers(self, watchers):
974 """Set the list of email addresses that should be cc'd based on the changed
975 files in this CL.
976 """
977 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978
979 def GetBranch(self):
980 """Returns the short branch name, e.g. 'master'."""
981 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000982 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000983 if not branchref:
984 return None
985 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986 self.branch = ShortBranchName(self.branchref)
987 return self.branch
988
989 def GetBranchRef(self):
990 """Returns the full branch name, e.g. 'refs/heads/master'."""
991 self.GetBranch() # Poke the lazy loader.
992 return self.branchref
993
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000994 @staticmethod
995 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000996 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997 e.g. 'origin', 'refs/heads/master'
998 """
999 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1001 error_ok=True).strip()
1002 if upstream_branch:
1003 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1004 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001005 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1006 error_ok=True).strip()
1007 if upstream_branch:
1008 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001010 # Fall back on trying a git-svn upstream branch.
1011 if settings.GetIsGitSvn():
1012 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001014 # Else, try to guess the origin remote.
1015 remote_branches = RunGit(['branch', '-r']).split()
1016 if 'origin/master' in remote_branches:
1017 # Fall back on origin/master if it exits.
1018 remote = 'origin'
1019 upstream_branch = 'refs/heads/master'
1020 elif 'origin/trunk' in remote_branches:
1021 # Fall back on origin/trunk if it exists. Generally a shared
1022 # git-svn clone
1023 remote = 'origin'
1024 upstream_branch = 'refs/heads/trunk'
1025 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 DieWithError(
1027 'Unable to determine default branch to diff against.\n'
1028 'Either pass complete "git diff"-style arguments, like\n'
1029 ' git cl upload origin/master\n'
1030 'or verify this branch is set up to track another \n'
1031 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001032
1033 return remote, upstream_branch
1034
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001035 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001036 upstream_branch = self.GetUpstreamBranch()
1037 if not BranchExists(upstream_branch):
1038 DieWithError('The upstream for the current branch (%s) does not exist '
1039 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001040 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001041 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 def GetUpstreamBranch(self):
1044 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001045 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001047 upstream_branch = upstream_branch.replace('refs/heads/',
1048 'refs/remotes/%s/' % remote)
1049 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1050 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 self.upstream_branch = upstream_branch
1052 return self.upstream_branch
1053
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001054 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001055 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001056 remote, branch = None, self.GetBranch()
1057 seen_branches = set()
1058 while branch not in seen_branches:
1059 seen_branches.add(branch)
1060 remote, branch = self.FetchUpstreamTuple(branch)
1061 branch = ShortBranchName(branch)
1062 if remote != '.' or branch.startswith('refs/remotes'):
1063 break
1064 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001065 remotes = RunGit(['remote'], error_ok=True).split()
1066 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001067 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001068 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001069 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001070 logging.warning('Could not determine which remote this change is '
1071 'associated with, so defaulting to "%s". This may '
1072 'not be what you want. You may prevent this message '
1073 'by running "git svn info" as documented here: %s',
1074 self._remote,
1075 GIT_INSTRUCTIONS_URL)
1076 else:
1077 logging.warn('Could not determine which remote this change is '
1078 'associated with. You may prevent this message by '
1079 'running "git svn info" as documented here: %s',
1080 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001081 branch = 'HEAD'
1082 if branch.startswith('refs/remotes'):
1083 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001084 elif branch.startswith('refs/branch-heads/'):
1085 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 else:
1087 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001088 return self._remote
1089
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001090 def GitSanityChecks(self, upstream_git_obj):
1091 """Checks git repo status and ensures diff is from local commits."""
1092
sbc@chromium.org79706062015-01-14 21:18:12 +00001093 if upstream_git_obj is None:
1094 if self.GetBranch() is None:
1095 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001096 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001097 else:
1098 print >> sys.stderr, (
1099 'ERROR: no upstream branch')
1100 return False
1101
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001102 # Verify the commit we're diffing against is in our current branch.
1103 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1104 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1105 if upstream_sha != common_ancestor:
1106 print >> sys.stderr, (
1107 'ERROR: %s is not in the current branch. You may need to rebase '
1108 'your tracking branch' % upstream_sha)
1109 return False
1110
1111 # List the commits inside the diff, and verify they are all local.
1112 commits_in_diff = RunGit(
1113 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1114 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1115 remote_branch = remote_branch.strip()
1116 if code != 0:
1117 _, remote_branch = self.GetRemoteBranch()
1118
1119 commits_in_remote = RunGit(
1120 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1121
1122 common_commits = set(commits_in_diff) & set(commits_in_remote)
1123 if common_commits:
1124 print >> sys.stderr, (
1125 'ERROR: Your diff contains %d commits already in %s.\n'
1126 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1127 'the diff. If you are using a custom git flow, you can override'
1128 ' the reference used for this check with "git config '
1129 'gitcl.remotebranch <git-ref>".' % (
1130 len(common_commits), remote_branch, upstream_git_obj))
1131 return False
1132 return True
1133
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001134 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001135 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001136
1137 Returns None if it is not set.
1138 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001139 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1140 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001141
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001142 def GetGitSvnRemoteUrl(self):
1143 """Return the configured git-svn remote URL parsed from git svn info.
1144
1145 Returns None if it is not set.
1146 """
1147 # URL is dependent on the current directory.
1148 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1149 if data:
1150 keys = dict(line.split(': ', 1) for line in data.splitlines()
1151 if ': ' in line)
1152 return keys.get('URL', None)
1153 return None
1154
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 def GetRemoteUrl(self):
1156 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1157
1158 Returns None if there is no remote.
1159 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001160 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001161 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1162
1163 # If URL is pointing to a local directory, it is probably a git cache.
1164 if os.path.isdir(url):
1165 url = RunGit(['config', 'remote.%s.url' % remote],
1166 error_ok=True,
1167 cwd=url).strip()
1168 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001170 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001171 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001172 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001173 issue = RunGit(['config',
1174 self._codereview_impl.IssueSetting(self.GetBranch())],
1175 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001176 self.issue = int(issue) or None if issue else None
1177 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 return self.issue
1179
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 def GetIssueURL(self):
1181 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001182 issue = self.GetIssue()
1183 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001184 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001185 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
1187 def GetDescription(self, pretty=False):
1188 if not self.has_description:
1189 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191 self.has_description = True
1192 if pretty:
1193 wrapper = textwrap.TextWrapper()
1194 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1195 return wrapper.fill(self.description)
1196 return self.description
1197
1198 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001199 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001200 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001201 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001203 self.patchset = int(patchset) or None if patchset else None
1204 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205 return self.patchset
1206
1207 def SetPatchset(self, patchset):
1208 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001209 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001211 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001212 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001215 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001216 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001218 def SetIssue(self, issue=None):
1219 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001220 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1221 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001223 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001224 RunGit(['config', issue_setting, str(issue)])
1225 codereview_server = self._codereview_impl.GetCodereviewServer()
1226 if codereview_server:
1227 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001229 current_issue = self.GetIssue()
1230 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001231 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 self.issue = None
1233 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001235 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001236 if not self.GitSanityChecks(upstream_branch):
1237 DieWithError('\nGit sanity check failure')
1238
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001239 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001240 if not root:
1241 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001242 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001243
1244 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001245 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001246 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001247 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001248 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001249 except subprocess2.CalledProcessError:
1250 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001251 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001252 'This branch probably doesn\'t exist anymore. To reset the\n'
1253 'tracking branch, please run\n'
1254 ' git branch --set-upstream %s trunk\n'
1255 'replacing trunk with origin/master or the relevant branch') %
1256 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001257
maruel@chromium.org52424302012-08-29 15:14:30 +00001258 issue = self.GetIssue()
1259 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001260 if issue:
1261 description = self.GetDescription()
1262 else:
1263 # If the change was never uploaded, use the log messages of all commits
1264 # up to the branch point, as git cl upload will prefill the description
1265 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001266 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1267 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001268
1269 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001270 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001271 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001272 name,
1273 description,
1274 absroot,
1275 files,
1276 issue,
1277 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001278 author,
1279 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001280
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001281 def UpdateDescription(self, description):
1282 self.description = description
1283 return self._codereview_impl.UpdateDescriptionRemote(description)
1284
1285 def RunHook(self, committing, may_prompt, verbose, change):
1286 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1287 try:
1288 return presubmit_support.DoPresubmitChecks(change, committing,
1289 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1290 default_presubmit=None, may_prompt=may_prompt,
1291 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1292 except presubmit_support.PresubmitFailure, e:
1293 DieWithError(
1294 ('%s\nMaybe your depot_tools is out of date?\n'
1295 'If all fails, contact maruel@') % e)
1296
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00001297 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1298 """Fetches and applies the issue patch from codereview to local branch."""
1299 if issue_arg.isdigit():
1300 parsed_issue_arg = _RietveldParsedIssueNumberArgument(int(issue_arg))
1301 else:
1302 # Assume url.
1303 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1304 urlparse.urlparse(issue_arg))
1305 if not parsed_issue_arg or not parsed_issue_arg.valid:
1306 DieWithError('Failed to parse issue argument "%s". '
1307 'Must be an issue number or a valid URL.' % issue_arg)
1308 return self._codereview_impl.CMDPatchWithParsedIssue(
1309 parsed_issue_arg, reject, nocommit, directory)
1310
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001311 # Forward methods to codereview specific implementation.
1312
1313 def CloseIssue(self):
1314 return self._codereview_impl.CloseIssue()
1315
1316 def GetStatus(self):
1317 return self._codereview_impl.GetStatus()
1318
1319 def GetCodereviewServer(self):
1320 return self._codereview_impl.GetCodereviewServer()
1321
1322 def GetApprovingReviewers(self):
1323 return self._codereview_impl.GetApprovingReviewers()
1324
1325 def GetMostRecentPatchset(self):
1326 return self._codereview_impl.GetMostRecentPatchset()
1327
1328 def __getattr__(self, attr):
1329 # This is because lots of untested code accesses Rietveld-specific stuff
1330 # directly, and it's hard to fix for sure. So, just let it work, and fix
1331 # on a cases by case basis.
1332 return getattr(self._codereview_impl, attr)
1333
1334
1335class _ChangelistCodereviewBase(object):
1336 """Abstract base class encapsulating codereview specifics of a changelist."""
1337 def __init__(self, changelist):
1338 self._changelist = changelist # instance of Changelist
1339
1340 def __getattr__(self, attr):
1341 # Forward methods to changelist.
1342 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1343 # _RietveldChangelistImpl to avoid this hack?
1344 return getattr(self._changelist, attr)
1345
1346 def GetStatus(self):
1347 """Apply a rough heuristic to give a simple summary of an issue's review
1348 or CQ status, assuming adherence to a common workflow.
1349
1350 Returns None if no issue for this branch, or specific string keywords.
1351 """
1352 raise NotImplementedError()
1353
1354 def GetCodereviewServer(self):
1355 """Returns server URL without end slash, like "https://codereview.com"."""
1356 raise NotImplementedError()
1357
1358 def FetchDescription(self):
1359 """Fetches and returns description from the codereview server."""
1360 raise NotImplementedError()
1361
1362 def GetCodereviewServerSetting(self):
1363 """Returns git config setting for the codereview server."""
1364 raise NotImplementedError()
1365
1366 @staticmethod
1367 def IssueSetting(branch):
1368 """Returns name of git config setting which stores issue number for a given
1369 branch."""
1370 raise NotImplementedError()
1371
1372 def PatchsetSetting(self):
1373 """Returns name of git config setting which stores issue number."""
1374 raise NotImplementedError()
1375
1376 def GetRieveldObjForPresubmit(self):
1377 # This is an unfortunate Rietveld-embeddedness in presubmit.
1378 # For non-Rietveld codereviews, this probably should return a dummy object.
1379 raise NotImplementedError()
1380
1381 def UpdateDescriptionRemote(self, description):
1382 """Update the description on codereview site."""
1383 raise NotImplementedError()
1384
1385 def CloseIssue(self):
1386 """Closes the issue."""
1387 raise NotImplementedError()
1388
1389 def GetApprovingReviewers(self):
1390 """Returns a list of reviewers approving the change.
1391
1392 Note: not necessarily committers.
1393 """
1394 raise NotImplementedError()
1395
1396 def GetMostRecentPatchset(self):
1397 """Returns the most recent patchset number from the codereview site."""
1398 raise NotImplementedError()
1399
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00001400 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1401 directory):
1402 """Fetches and applies the issue.
1403
1404 Arguments:
1405 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1406 reject: if True, reject the failed patch instead of switching to 3-way
1407 merge. Rietveld only.
1408 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1409 only.
1410 directory: switch to directory before applying the patch. Rietveld only.
1411 """
1412 raise NotImplementedError()
1413
1414 @staticmethod
1415 def ParseIssueURL(parsed_url):
1416 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1417 failed."""
1418 raise NotImplementedError()
1419
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001420
1421class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1422 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1423 super(_RietveldChangelistImpl, self).__init__(changelist)
1424 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1425 settings.GetDefaultServerUrl()
1426
1427 self._rietveld_server = rietveld_server
1428 self._auth_config = auth_config
1429 self._props = None
1430 self._rpc_server = None
1431
1432 def GetAuthConfig(self):
1433 return self._auth_config
1434
1435 def GetCodereviewServer(self):
1436 if not self._rietveld_server:
1437 # If we're on a branch then get the server potentially associated
1438 # with that branch.
1439 if self.GetIssue():
1440 rietveld_server_setting = self.GetCodereviewServerSetting()
1441 if rietveld_server_setting:
1442 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1443 ['config', rietveld_server_setting], error_ok=True).strip())
1444 if not self._rietveld_server:
1445 self._rietveld_server = settings.GetDefaultServerUrl()
1446 return self._rietveld_server
1447
1448 def FetchDescription(self):
1449 issue = self.GetIssue()
1450 assert issue
1451 try:
1452 return self.RpcServer().get_description(issue).strip()
1453 except urllib2.HTTPError as e:
1454 if e.code == 404:
1455 DieWithError(
1456 ('\nWhile fetching the description for issue %d, received a '
1457 '404 (not found)\n'
1458 'error. It is likely that you deleted this '
1459 'issue on the server. If this is the\n'
1460 'case, please run\n\n'
1461 ' git cl issue 0\n\n'
1462 'to clear the association with the deleted issue. Then run '
1463 'this command again.') % issue)
1464 else:
1465 DieWithError(
1466 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1467 except urllib2.URLError as e:
1468 print >> sys.stderr, (
1469 'Warning: Failed to retrieve CL description due to network '
1470 'failure.')
1471 return ''
1472
1473 def GetMostRecentPatchset(self):
1474 return self.GetIssueProperties()['patchsets'][-1]
1475
1476 def GetPatchSetDiff(self, issue, patchset):
1477 return self.RpcServer().get(
1478 '/download/issue%s_%s.diff' % (issue, patchset))
1479
1480 def GetIssueProperties(self):
1481 if self._props is None:
1482 issue = self.GetIssue()
1483 if not issue:
1484 self._props = {}
1485 else:
1486 self._props = self.RpcServer().get_issue_properties(issue, True)
1487 return self._props
1488
1489 def GetApprovingReviewers(self):
1490 return get_approving_reviewers(self.GetIssueProperties())
1491
1492 def AddComment(self, message):
1493 return self.RpcServer().add_comment(self.GetIssue(), message)
1494
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001495 def GetStatus(self):
1496 """Apply a rough heuristic to give a simple summary of an issue's review
1497 or CQ status, assuming adherence to a common workflow.
1498
1499 Returns None if no issue for this branch, or one of the following keywords:
1500 * 'error' - error from review tool (including deleted issues)
1501 * 'unsent' - not sent for review
1502 * 'waiting' - waiting for review
1503 * 'reply' - waiting for owner to reply to review
1504 * 'lgtm' - LGTM from at least one approved reviewer
1505 * 'commit' - in the commit queue
1506 * 'closed' - closed
1507 """
1508 if not self.GetIssue():
1509 return None
1510
1511 try:
1512 props = self.GetIssueProperties()
1513 except urllib2.HTTPError:
1514 return 'error'
1515
1516 if props.get('closed'):
1517 # Issue is closed.
1518 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001519 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001520 # Issue is in the commit queue.
1521 return 'commit'
1522
1523 try:
1524 reviewers = self.GetApprovingReviewers()
1525 except urllib2.HTTPError:
1526 return 'error'
1527
1528 if reviewers:
1529 # Was LGTM'ed.
1530 return 'lgtm'
1531
1532 messages = props.get('messages') or []
1533
1534 if not messages:
1535 # No message was sent.
1536 return 'unsent'
1537 if messages[-1]['sender'] != props.get('owner_email'):
1538 # Non-LGTM reply from non-owner
1539 return 'reply'
1540 return 'waiting'
1541
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001542 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001543 return self.RpcServer().update_description(
1544 self.GetIssue(), self.description)
1545
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001547 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001549 def SetFlag(self, flag, value):
1550 """Patchset must match."""
1551 if not self.GetPatchset():
1552 DieWithError('The patchset needs to match. Send another patchset.')
1553 try:
1554 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001555 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001556 except urllib2.HTTPError, e:
1557 if e.code == 404:
1558 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1559 if e.code == 403:
1560 DieWithError(
1561 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1562 'match?') % (self.GetIssue(), self.GetPatchset()))
1563 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001565 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566 """Returns an upload.RpcServer() to access this review's rietveld instance.
1567 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001568 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001569 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001570 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001571 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001572 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001573
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001574 @staticmethod
1575 def IssueSetting(branch):
1576 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001578 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579 """Return the git setting that stores this change's most recent patchset."""
1580 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1581
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001582 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001584 branch = self.GetBranch()
1585 if branch:
1586 return 'branch.%s.rietveldserver' % branch
1587 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001589 def GetRieveldObjForPresubmit(self):
1590 return self.RpcServer()
1591
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00001592 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1593 directory):
1594 # TODO(maruel): Use apply_issue.py
1595
1596 # PatchIssue should never be called with a dirty tree. It is up to the
1597 # caller to check this, but just in case we assert here since the
1598 # consequences of the caller not checking this could be dire.
1599 assert(not git_common.is_dirty_git_tree('apply'))
1600 assert(parsed_issue_arg.valid)
1601 self._changelist.issue = parsed_issue_arg.issue
1602 if parsed_issue_arg.hostname:
1603 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1604
1605 if parsed_issue_arg.patch_url:
1606 assert parsed_issue_arg.patchset
1607 patchset = parsed_issue_arg.patchset
1608 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1609 else:
1610 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1611 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1612
1613 # Switch up to the top-level directory, if necessary, in preparation for
1614 # applying the patch.
1615 top = settings.GetRelativeRoot()
1616 if top:
1617 os.chdir(top)
1618
1619 # Git patches have a/ at the beginning of source paths. We strip that out
1620 # with a sed script rather than the -p flag to patch so we can feed either
1621 # Git or svn-style patches into the same apply command.
1622 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1623 try:
1624 patch_data = subprocess2.check_output(
1625 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1626 except subprocess2.CalledProcessError:
1627 DieWithError('Git patch mungling failed.')
1628 logging.info(patch_data)
1629
1630 # We use "git apply" to apply the patch instead of "patch" so that we can
1631 # pick up file adds.
1632 # The --index flag means: also insert into the index (so we catch adds).
1633 cmd = ['git', 'apply', '--index', '-p0']
1634 if directory:
1635 cmd.extend(('--directory', directory))
1636 if reject:
1637 cmd.append('--reject')
1638 elif IsGitVersionAtLeast('1.7.12'):
1639 cmd.append('--3way')
1640 try:
1641 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1642 stdin=patch_data, stdout=subprocess2.VOID)
1643 except subprocess2.CalledProcessError:
1644 print 'Failed to apply the patch'
1645 return 1
1646
1647 # If we had an issue, commit the current state and register the issue.
1648 if not nocommit:
1649 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1650 'patch from issue %(i)s at patchset '
1651 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1652 % {'i': self.GetIssue(), 'p': patchset})])
1653 self.SetIssue(self.GetIssue())
1654 self.SetPatchset(patchset)
1655 print "Committed patch locally."
1656 else:
1657 print "Patch applied to index."
1658 return 0
1659
1660 @staticmethod
1661 def ParseIssueURL(parsed_url):
1662 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1663 return None
1664 # Typical url: https://domain/<issue_number>[/[other]]
1665 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1666 if match:
1667 return _RietveldParsedIssueNumberArgument(
1668 issue=int(match.group(1)),
1669 hostname=parsed_url.netloc)
1670 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1671 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1672 if match:
1673 return _RietveldParsedIssueNumberArgument(
1674 issue=int(match.group(1)),
1675 patchset=int(match.group(2)),
1676 hostname=parsed_url.netloc,
1677 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1678 return None
1679
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001680
1681class _GerritChangelistImpl(_ChangelistCodereviewBase):
1682 def __init__(self, changelist, auth_config=None):
1683 # auth_config is Rietveld thing, kept here to preserve interface only.
1684 super(_GerritChangelistImpl, self).__init__(changelist)
1685 self._change_id = None
1686 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1687 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1688
1689 def _GetGerritHost(self):
1690 # Lazy load of configs.
1691 self.GetCodereviewServer()
1692 return self._gerrit_host
1693
1694 def GetCodereviewServer(self):
1695 if not self._gerrit_server:
1696 # If we're on a branch then get the server potentially associated
1697 # with that branch.
1698 if self.GetIssue():
1699 gerrit_server_setting = self.GetCodereviewServerSetting()
1700 if gerrit_server_setting:
1701 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1702 error_ok=True).strip()
1703 if self._gerrit_server:
1704 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1705 if not self._gerrit_server:
1706 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1707 # has "-review" suffix for lowest level subdomain.
1708 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1709 parts[0] = parts[0] + '-review'
1710 self._gerrit_host = '.'.join(parts)
1711 self._gerrit_server = 'https://%s' % self._gerrit_host
1712 return self._gerrit_server
1713
1714 @staticmethod
1715 def IssueSetting(branch):
1716 return 'branch.%s.gerritissue' % branch
1717
1718 def PatchsetSetting(self):
1719 """Return the git setting that stores this change's most recent patchset."""
1720 return 'branch.%s.gerritpatchset' % self.GetBranch()
1721
1722 def GetCodereviewServerSetting(self):
1723 """Returns the git setting that stores this change's Gerrit server."""
1724 branch = self.GetBranch()
1725 if branch:
1726 return 'branch.%s.gerritserver' % branch
1727 return None
1728
1729 def GetRieveldObjForPresubmit(self):
1730 class ThisIsNotRietveldIssue(object):
1731 def __nonzero__(self):
1732 # This is a hack to make presubmit_support think that rietveld is not
1733 # defined, yet still ensure that calls directly result in a decent
1734 # exception message below.
1735 return False
1736
1737 def __getattr__(self, attr):
1738 print(
1739 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1740 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1741 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1742 'or use Rietveld for codereview.\n'
1743 'See also http://crbug.com/579160.' % attr)
1744 raise NotImplementedError()
1745 return ThisIsNotRietveldIssue()
1746
1747 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001748 """Apply a rough heuristic to give a simple summary of an issue's review
1749 or CQ status, assuming adherence to a common workflow.
1750
1751 Returns None if no issue for this branch, or one of the following keywords:
1752 * 'error' - error from review tool (including deleted issues)
1753 * 'unsent' - no reviewers added
1754 * 'waiting' - waiting for review
1755 * 'reply' - waiting for owner to reply to review
1756 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1757 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1758 * 'commit' - in the commit queue
1759 * 'closed' - abandoned
1760 """
1761 if not self.GetIssue():
1762 return None
1763
1764 try:
1765 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1766 except httplib.HTTPException:
1767 return 'error'
1768
1769 if data['status'] == 'ABANDONED':
1770 return 'closed'
1771
1772 cq_label = data['labels'].get('Commit-Queue', {})
1773 if cq_label:
1774 # Vote value is a stringified integer, which we expect from 0 to 2.
1775 vote_value = cq_label.get('value', '0')
1776 vote_text = cq_label.get('values', {}).get(vote_value, '')
1777 if vote_text.lower() == 'commit':
1778 return 'commit'
1779
1780 lgtm_label = data['labels'].get('Code-Review', {})
1781 if lgtm_label:
1782 if 'rejected' in lgtm_label:
1783 return 'not lgtm'
1784 if 'approved' in lgtm_label:
1785 return 'lgtm'
1786
1787 if not data.get('reviewers', {}).get('REVIEWER', []):
1788 return 'unsent'
1789
1790 messages = data.get('messages', [])
1791 if messages:
1792 owner = data['owner'].get('_account_id')
1793 last_message_author = messages[-1].get('author', {}).get('_account_id')
1794 if owner != last_message_author:
1795 # Some reply from non-owner.
1796 return 'reply'
1797
1798 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001799
1800 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001801 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 return data['revisions'][data['current_revision']]['_number']
1803
1804 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001805 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001806 return data['revisions'][data['current_revision']]['commit_with_footers']
1807
1808 def UpdateDescriptionRemote(self, description):
1809 # TODO(tandrii)
1810 raise NotImplementedError()
1811
1812 def CloseIssue(self):
1813 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1814
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001815 def SubmitIssue(self, wait_for_merge=True):
1816 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
1817 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001819 def _GetChangeDetail(self, options):
1820 return gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1821 options)
1822
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001823 def CMDLand(self, force, bypass_hooks, verbose):
1824 if git_common.is_dirty_git_tree('land'):
1825 return 1
1826 differs = True
1827 last_upload = RunGit(['config',
1828 'branch.%s.gerritsquashhash' % self.GetBranch()],
1829 error_ok=True).strip()
1830 # Note: git diff outputs nothing if there is no diff.
1831 if not last_upload or RunGit(['diff', last_upload]).strip():
1832 print('WARNING: some changes from local branch haven\'t been uploaded')
1833 else:
1834 detail = self._GetChangeDetail(['CURRENT_REVISION'])
1835 if detail['current_revision'] == last_upload:
1836 differs = False
1837 else:
1838 print('WARNING: local branch contents differ from latest uploaded '
1839 'patchset')
1840 if differs:
1841 if not force:
1842 ask_for_data(
1843 'Do you want to submit latest Gerrit patchset and bypass hooks?')
1844 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
1845 elif not bypass_hooks:
1846 hook_results = self.RunHook(
1847 committing=True,
1848 may_prompt=not force,
1849 verbose=verbose,
1850 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
1851 if not hook_results.should_continue():
1852 return 1
1853
1854 self.SubmitIssue(wait_for_merge=True)
1855 print('Issue %s has been submitted.' % self.GetIssueURL())
1856 return 0
1857
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00001858 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1859 directory):
1860 assert not reject
1861 assert not nocommit
1862 assert not directory
1863 assert parsed_issue_arg.valid
1864
1865 self._changelist.issue = parsed_issue_arg.issue
1866
1867 if parsed_issue_arg.hostname:
1868 self._gerrit_host = parsed_issue_arg.hostname
1869 self._gerrit_server = 'https://%s' % self._gerrit_host
1870
1871 detail = self._GetChangeDetail(['ALL_REVISIONS'])
1872
1873 if not parsed_issue_arg.patchset:
1874 # Use current revision by default.
1875 revision_info = detail['revisions'][detail['current_revision']]
1876 patchset = int(revision_info['_number'])
1877 else:
1878 patchset = parsed_issue_arg.patchset
1879 for revision_info in detail['revisions'].itervalues():
1880 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1881 break
1882 else:
1883 DieWithError('Couldn\'t find patchset %i in issue %i' %
1884 (parsed_issue_arg.patchset, self.GetIssue()))
1885
1886 fetch_info = revision_info['fetch']['http']
1887 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
1888 RunGit(['cherry-pick', 'FETCH_HEAD'])
1889 self.SetIssue(self.GetIssue())
1890 self.SetPatchset(patchset)
1891 print('Committed patch for issue %i pathset %i locally' %
1892 (self.GetIssue(), self.GetPatchset()))
1893 return 0
1894
1895 @staticmethod
1896 def ParseIssueURL(parsed_url):
1897 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1898 return None
1899 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
1900 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
1901 # Short urls like https://domain/<issue_number> can be used, but don't allow
1902 # specifying the patchset (you'd 404), but we allow that here.
1903 if parsed_url.path == '/':
1904 part = parsed_url.fragment
1905 else:
1906 part = parsed_url.path
1907 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
1908 if match:
1909 return _ParsedIssueNumberArgument(
1910 issue=int(match.group(2)),
1911 patchset=int(match.group(4)) if match.group(4) else None,
1912 hostname=parsed_url.netloc)
1913 return None
1914
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001915
1916_CODEREVIEW_IMPLEMENTATIONS = {
1917 'rietveld': _RietveldChangelistImpl,
1918 'gerrit': _GerritChangelistImpl,
1919}
1920
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001921
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001922class ChangeDescription(object):
1923 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001924 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001925 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001926
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001927 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001928 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001929
agable@chromium.org42c20792013-09-12 17:34:49 +00001930 @property # www.logilab.org/ticket/89786
1931 def description(self): # pylint: disable=E0202
1932 return '\n'.join(self._description_lines)
1933
1934 def set_description(self, desc):
1935 if isinstance(desc, basestring):
1936 lines = desc.splitlines()
1937 else:
1938 lines = [line.rstrip() for line in desc]
1939 while lines and not lines[0]:
1940 lines.pop(0)
1941 while lines and not lines[-1]:
1942 lines.pop(-1)
1943 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001944
piman@chromium.org336f9122014-09-04 02:16:55 +00001945 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001946 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001947 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001948 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001949 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001950 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001951
agable@chromium.org42c20792013-09-12 17:34:49 +00001952 # Get the set of R= and TBR= lines and remove them from the desciption.
1953 regexp = re.compile(self.R_LINE)
1954 matches = [regexp.match(line) for line in self._description_lines]
1955 new_desc = [l for i, l in enumerate(self._description_lines)
1956 if not matches[i]]
1957 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001958
agable@chromium.org42c20792013-09-12 17:34:49 +00001959 # Construct new unified R= and TBR= lines.
1960 r_names = []
1961 tbr_names = []
1962 for match in matches:
1963 if not match:
1964 continue
1965 people = cleanup_list([match.group(2).strip()])
1966 if match.group(1) == 'TBR':
1967 tbr_names.extend(people)
1968 else:
1969 r_names.extend(people)
1970 for name in r_names:
1971 if name not in reviewers:
1972 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001973 if add_owners_tbr:
1974 owners_db = owners.Database(change.RepositoryRoot(),
1975 fopen=file, os_path=os.path, glob=glob.glob)
1976 all_reviewers = set(tbr_names + reviewers)
1977 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1978 all_reviewers)
1979 tbr_names.extend(owners_db.reviewers_for(missing_files,
1980 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001981 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1982 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1983
1984 # Put the new lines in the description where the old first R= line was.
1985 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1986 if 0 <= line_loc < len(self._description_lines):
1987 if new_tbr_line:
1988 self._description_lines.insert(line_loc, new_tbr_line)
1989 if new_r_line:
1990 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001991 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001992 if new_r_line:
1993 self.append_footer(new_r_line)
1994 if new_tbr_line:
1995 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001996
1997 def prompt(self):
1998 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001999 self.set_description([
2000 '# Enter a description of the change.',
2001 '# This will be displayed on the codereview site.',
2002 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002003 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002004 '--------------------',
2005 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002006
agable@chromium.org42c20792013-09-12 17:34:49 +00002007 regexp = re.compile(self.BUG_LINE)
2008 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002009 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002010 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002011 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002012 if not content:
2013 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002014 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002015
2016 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002017 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2018 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002019 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002020 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002021
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002022 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002023 if self._description_lines:
2024 # Add an empty line if either the last line or the new line isn't a tag.
2025 last_line = self._description_lines[-1]
2026 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2027 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2028 self._description_lines.append('')
2029 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002030
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002031 def get_reviewers(self):
2032 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002033 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2034 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002035 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002036
2037
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002038def get_approving_reviewers(props):
2039 """Retrieves the reviewers that approved a CL from the issue properties with
2040 messages.
2041
2042 Note that the list may contain reviewers that are not committer, thus are not
2043 considered by the CQ.
2044 """
2045 return sorted(
2046 set(
2047 message['sender']
2048 for message in props['messages']
2049 if message['approval'] and message['sender'] in props['reviewers']
2050 )
2051 )
2052
2053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054def FindCodereviewSettingsFile(filename='codereview.settings'):
2055 """Finds the given file starting in the cwd and going up.
2056
2057 Only looks up to the top of the repository unless an
2058 'inherit-review-settings-ok' file exists in the root of the repository.
2059 """
2060 inherit_ok_file = 'inherit-review-settings-ok'
2061 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002062 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2064 root = '/'
2065 while True:
2066 if filename in os.listdir(cwd):
2067 if os.path.isfile(os.path.join(cwd, filename)):
2068 return open(os.path.join(cwd, filename))
2069 if cwd == root:
2070 break
2071 cwd = os.path.dirname(cwd)
2072
2073
2074def LoadCodereviewSettingsFromFile(fileobj):
2075 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002076 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002078 def SetProperty(name, setting, unset_error_ok=False):
2079 fullname = 'rietveld.' + name
2080 if setting in keyvals:
2081 RunGit(['config', fullname, keyvals[setting]])
2082 else:
2083 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2084
2085 SetProperty('server', 'CODE_REVIEW_SERVER')
2086 # Only server setting is required. Other settings can be absent.
2087 # In that case, we ignore errors raised during option deletion attempt.
2088 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002089 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2091 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002092 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002093 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002094 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2095 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002096 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002097 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002098 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002099 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2100 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002102 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002103 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002104
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002105 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2106 RunGit(['config', 'gerrit.squash-uploads',
2107 keyvals['GERRIT_SQUASH_UPLOADS']])
2108
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2110 #should be of the form
2111 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2112 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2113 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2114 keyvals['ORIGIN_URL_CONFIG']])
2115
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002116
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002117def urlretrieve(source, destination):
2118 """urllib is broken for SSL connections via a proxy therefore we
2119 can't use urllib.urlretrieve()."""
2120 with open(destination, 'w') as f:
2121 f.write(urllib2.urlopen(source).read())
2122
2123
ukai@chromium.org712d6102013-11-27 00:52:58 +00002124def hasSheBang(fname):
2125 """Checks fname is a #! script."""
2126 with open(fname) as f:
2127 return f.read(2).startswith('#!')
2128
2129
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002130def DownloadGerritHook(force):
2131 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002132
2133 Args:
2134 force: True to update hooks. False to install hooks if not present.
2135 """
2136 if not settings.GetIsGerrit():
2137 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002138 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002139 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2140 if not os.access(dst, os.X_OK):
2141 if os.path.exists(dst):
2142 if not force:
2143 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002144 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002145 print(
2146 'WARNING: installing Gerrit commit-msg hook.\n'
2147 ' This behavior of git cl will soon be disabled.\n'
2148 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002149 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002150 if not hasSheBang(dst):
2151 DieWithError('Not a script: %s\n'
2152 'You need to download from\n%s\n'
2153 'into .git/hooks/commit-msg and '
2154 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002155 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2156 except Exception:
2157 if os.path.exists(dst):
2158 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002159 DieWithError('\nFailed to download hooks.\n'
2160 'You need to download from\n%s\n'
2161 'into .git/hooks/commit-msg and '
2162 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002163
2164
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002165
2166def GetRietveldCodereviewSettingsInteractively():
2167 """Prompt the user for settings."""
2168 server = settings.GetDefaultServerUrl(error_ok=True)
2169 prompt = 'Rietveld server (host[:port])'
2170 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2171 newserver = ask_for_data(prompt + ':')
2172 if not server and not newserver:
2173 newserver = DEFAULT_SERVER
2174 if newserver:
2175 newserver = gclient_utils.UpgradeToHttps(newserver)
2176 if newserver != server:
2177 RunGit(['config', 'rietveld.server', newserver])
2178
2179 def SetProperty(initial, caption, name, is_url):
2180 prompt = caption
2181 if initial:
2182 prompt += ' ("x" to clear) [%s]' % initial
2183 new_val = ask_for_data(prompt + ':')
2184 if new_val == 'x':
2185 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2186 elif new_val:
2187 if is_url:
2188 new_val = gclient_utils.UpgradeToHttps(new_val)
2189 if new_val != initial:
2190 RunGit(['config', 'rietveld.' + name, new_val])
2191
2192 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2193 SetProperty(settings.GetDefaultPrivateFlag(),
2194 'Private flag (rietveld only)', 'private', False)
2195 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2196 'tree-status-url', False)
2197 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2198 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2199 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2200 'run-post-upload-hook', False)
2201
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002202@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002204 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002205
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002206 print('WARNING: git cl config works for Rietveld only.\n'
2207 'For Gerrit, see http://crbug.com/579160.')
2208 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002209 parser.add_option('--activate-update', action='store_true',
2210 help='activate auto-updating [rietveld] section in '
2211 '.git/config')
2212 parser.add_option('--deactivate-update', action='store_true',
2213 help='deactivate auto-updating [rietveld] section in '
2214 '.git/config')
2215 options, args = parser.parse_args(args)
2216
2217 if options.deactivate_update:
2218 RunGit(['config', 'rietveld.autoupdate', 'false'])
2219 return
2220
2221 if options.activate_update:
2222 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2223 return
2224
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002225 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002226 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002227 return 0
2228
2229 url = args[0]
2230 if not url.endswith('codereview.settings'):
2231 url = os.path.join(url, 'codereview.settings')
2232
2233 # Load code review settings and download hooks (if available).
2234 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2235 return 0
2236
2237
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002238def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002239 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002240 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2241 branch = ShortBranchName(branchref)
2242 _, args = parser.parse_args(args)
2243 if not args:
2244 print("Current base-url:")
2245 return RunGit(['config', 'branch.%s.base-url' % branch],
2246 error_ok=False).strip()
2247 else:
2248 print("Setting base-url to %s" % args[0])
2249 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2250 error_ok=False).strip()
2251
2252
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002253def color_for_status(status):
2254 """Maps a Changelist status to color, for CMDstatus and other tools."""
2255 return {
2256 'unsent': Fore.RED,
2257 'waiting': Fore.BLUE,
2258 'reply': Fore.YELLOW,
2259 'lgtm': Fore.GREEN,
2260 'commit': Fore.MAGENTA,
2261 'closed': Fore.CYAN,
2262 'error': Fore.WHITE,
2263 }.get(status, Fore.WHITE)
2264
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002265def fetch_cl_status(branch, auth_config=None):
2266 """Fetches information for an issue and returns (branch, issue, status)."""
2267 cl = Changelist(branchref=branch, auth_config=auth_config)
2268 url = cl.GetIssueURL()
2269 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002270
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002271 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002272 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002273 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002274
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002275 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002276
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002277def get_cl_statuses(
2278 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002279 """Returns a blocking iterable of (branch, issue, color) for given branches.
2280
2281 If fine_grained is true, this will fetch CL statuses from the server.
2282 Otherwise, simply indicate if there's a matching url for the given branches.
2283
2284 If max_processes is specified, it is used as the maximum number of processes
2285 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2286 spawned.
2287 """
2288 # Silence upload.py otherwise it becomes unwieldly.
2289 upload.verbosity = 0
2290
2291 if fine_grained:
2292 # Process one branch synchronously to work through authentication, then
2293 # spawn processes to process all the other branches in parallel.
2294 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002295 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2296 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002297
2298 branches_to_fetch = branches[1:]
2299 pool = ThreadPool(
2300 min(max_processes, len(branches_to_fetch))
2301 if max_processes is not None
2302 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002303 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002304 yield x
2305 else:
2306 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2307 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002308 cl = Changelist(branchref=b, auth_config=auth_config)
2309 url = cl.GetIssueURL()
2310 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002311
rmistry@google.com2dd99862015-06-22 12:22:18 +00002312
2313def upload_branch_deps(cl, args):
2314 """Uploads CLs of local branches that are dependents of the current branch.
2315
2316 If the local branch dependency tree looks like:
2317 test1 -> test2.1 -> test3.1
2318 -> test3.2
2319 -> test2.2 -> test3.3
2320
2321 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2322 run on the dependent branches in this order:
2323 test2.1, test3.1, test3.2, test2.2, test3.3
2324
2325 Note: This function does not rebase your local dependent branches. Use it when
2326 you make a change to the parent branch that will not conflict with its
2327 dependent branches, and you would like their dependencies updated in
2328 Rietveld.
2329 """
2330 if git_common.is_dirty_git_tree('upload-branch-deps'):
2331 return 1
2332
2333 root_branch = cl.GetBranch()
2334 if root_branch is None:
2335 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2336 'Get on a branch!')
2337 if not cl.GetIssue() or not cl.GetPatchset():
2338 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2339 'patchset dependencies without an uploaded CL.')
2340
2341 branches = RunGit(['for-each-ref',
2342 '--format=%(refname:short) %(upstream:short)',
2343 'refs/heads'])
2344 if not branches:
2345 print('No local branches found.')
2346 return 0
2347
2348 # Create a dictionary of all local branches to the branches that are dependent
2349 # on it.
2350 tracked_to_dependents = collections.defaultdict(list)
2351 for b in branches.splitlines():
2352 tokens = b.split()
2353 if len(tokens) == 2:
2354 branch_name, tracked = tokens
2355 tracked_to_dependents[tracked].append(branch_name)
2356
2357 print
2358 print 'The dependent local branches of %s are:' % root_branch
2359 dependents = []
2360 def traverse_dependents_preorder(branch, padding=''):
2361 dependents_to_process = tracked_to_dependents.get(branch, [])
2362 padding += ' '
2363 for dependent in dependents_to_process:
2364 print '%s%s' % (padding, dependent)
2365 dependents.append(dependent)
2366 traverse_dependents_preorder(dependent, padding)
2367 traverse_dependents_preorder(root_branch)
2368 print
2369
2370 if not dependents:
2371 print 'There are no dependent local branches for %s' % root_branch
2372 return 0
2373
2374 print ('This command will checkout all dependent branches and run '
2375 '"git cl upload".')
2376 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2377
andybons@chromium.org962f9462016-02-03 20:00:42 +00002378 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002379 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002380 args.extend(['-t', 'Updated patchset dependency'])
2381
rmistry@google.com2dd99862015-06-22 12:22:18 +00002382 # Record all dependents that failed to upload.
2383 failures = {}
2384 # Go through all dependents, checkout the branch and upload.
2385 try:
2386 for dependent_branch in dependents:
2387 print
2388 print '--------------------------------------'
2389 print 'Running "git cl upload" from %s:' % dependent_branch
2390 RunGit(['checkout', '-q', dependent_branch])
2391 print
2392 try:
2393 if CMDupload(OptionParser(), args) != 0:
2394 print 'Upload failed for %s!' % dependent_branch
2395 failures[dependent_branch] = 1
2396 except: # pylint: disable=W0702
2397 failures[dependent_branch] = 1
2398 print
2399 finally:
2400 # Swap back to the original root branch.
2401 RunGit(['checkout', '-q', root_branch])
2402
2403 print
2404 print 'Upload complete for dependent branches!'
2405 for dependent_branch in dependents:
2406 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2407 print ' %s : %s' % (dependent_branch, upload_status)
2408 print
2409
2410 return 0
2411
2412
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002413def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002414 """Show status of changelists.
2415
2416 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002417 - Red not sent for review or broken
2418 - Blue waiting for review
2419 - Yellow waiting for you to reply to review
2420 - Green LGTM'ed
2421 - Magenta in the commit queue
2422 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002423
2424 Also see 'git cl comments'.
2425 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002426 parser.add_option('--field',
2427 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002428 parser.add_option('-f', '--fast', action='store_true',
2429 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002430 parser.add_option(
2431 '-j', '--maxjobs', action='store', type=int,
2432 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002433
2434 auth.add_auth_options(parser)
2435 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002436 if args:
2437 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002438 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002439
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002440 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002441 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442 if options.field.startswith('desc'):
2443 print cl.GetDescription()
2444 elif options.field == 'id':
2445 issueid = cl.GetIssue()
2446 if issueid:
2447 print issueid
2448 elif options.field == 'patch':
2449 patchset = cl.GetPatchset()
2450 if patchset:
2451 print patchset
2452 elif options.field == 'url':
2453 url = cl.GetIssueURL()
2454 if url:
2455 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002456 return 0
2457
2458 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2459 if not branches:
2460 print('No local branch found.')
2461 return 0
2462
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002463 changes = (
2464 Changelist(branchref=b, auth_config=auth_config)
2465 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002466 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002467 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002468 alignment = max(5, max(len(b) for b in branches))
2469 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002470 output = get_cl_statuses(branches,
2471 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002472 max_processes=options.maxjobs,
2473 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002474
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002475 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002476 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002477 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002478 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002479 b, i, status = output.next()
2480 branch_statuses[b] = (i, status)
2481 issue_url, status = branch_statuses.pop(branch)
2482 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002483 reset = Fore.RESET
2484 if not sys.stdout.isatty():
2485 color = ''
2486 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002487 status_str = '(%s)' % status if status else ''
2488 print ' %*s : %s%s %s%s' % (
2489 alignment, ShortBranchName(branch), color, issue_url, status_str,
2490 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002491
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002492 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002493 print
2494 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002495 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002496 if not cl.GetIssue():
2497 print 'No issue assigned.'
2498 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002499 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002500 if not options.fast:
2501 print 'Issue description:'
2502 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002503 return 0
2504
2505
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002506def colorize_CMDstatus_doc():
2507 """To be called once in main() to add colors to git cl status help."""
2508 colors = [i for i in dir(Fore) if i[0].isupper()]
2509
2510 def colorize_line(line):
2511 for color in colors:
2512 if color in line.upper():
2513 # Extract whitespaces first and the leading '-'.
2514 indent = len(line) - len(line.lstrip(' ')) + 1
2515 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2516 return line
2517
2518 lines = CMDstatus.__doc__.splitlines()
2519 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2520
2521
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002522@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002523def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002524 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002525
2526 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002527 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002528 parser.add_option('-r', '--reverse', action='store_true',
2529 help='Lookup the branch(es) for the specified issues. If '
2530 'no issues are specified, all branches with mapped '
2531 'issues will be listed.')
2532 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002533
dnj@chromium.org406c4402015-03-03 17:22:28 +00002534 if options.reverse:
2535 branches = RunGit(['for-each-ref', 'refs/heads',
2536 '--format=%(refname:short)']).splitlines()
2537
2538 # Reverse issue lookup.
2539 issue_branch_map = {}
2540 for branch in branches:
2541 cl = Changelist(branchref=branch)
2542 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2543 if not args:
2544 args = sorted(issue_branch_map.iterkeys())
2545 for issue in args:
2546 if not issue:
2547 continue
2548 print 'Branch for issue number %s: %s' % (
2549 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2550 else:
2551 cl = Changelist()
2552 if len(args) > 0:
2553 try:
2554 issue = int(args[0])
2555 except ValueError:
2556 DieWithError('Pass a number to set the issue or none to list it.\n'
2557 'Maybe you want to run git cl status?')
2558 cl.SetIssue(issue)
2559 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002560 return 0
2561
2562
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002563def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002564 """Shows or posts review comments for any changelist."""
2565 parser.add_option('-a', '--add-comment', dest='comment',
2566 help='comment to add to an issue')
2567 parser.add_option('-i', dest='issue',
2568 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002569 parser.add_option('-j', '--json-file',
2570 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002571 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002572 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002573 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002574
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002575 issue = None
2576 if options.issue:
2577 try:
2578 issue = int(options.issue)
2579 except ValueError:
2580 DieWithError('A review issue id is expected to be a number')
2581
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002582 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002583
2584 if options.comment:
2585 cl.AddComment(options.comment)
2586 return 0
2587
2588 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002589 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002590 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002591 summary.append({
2592 'date': message['date'],
2593 'lgtm': False,
2594 'message': message['text'],
2595 'not_lgtm': False,
2596 'sender': message['sender'],
2597 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002598 if message['disapproval']:
2599 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002600 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002601 elif message['approval']:
2602 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002603 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002604 elif message['sender'] == data['owner_email']:
2605 color = Fore.MAGENTA
2606 else:
2607 color = Fore.BLUE
2608 print '\n%s%s %s%s' % (
2609 color, message['date'].split('.', 1)[0], message['sender'],
2610 Fore.RESET)
2611 if message['text'].strip():
2612 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002613 if options.json_file:
2614 with open(options.json_file, 'wb') as f:
2615 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002616 return 0
2617
2618
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002619def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002620 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002621 parser.add_option('-d', '--display', action='store_true',
2622 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002623 auth.add_auth_options(parser)
2624 options, _ = parser.parse_args(args)
2625 auth_config = auth.extract_auth_config_from_options(options)
2626 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002627 if not cl.GetIssue():
2628 DieWithError('This branch has no associated changelist.')
2629 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002630 if options.display:
2631 print description.description
2632 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002633 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002634 if cl.GetDescription() != description.description:
2635 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002636 return 0
2637
2638
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002639def CreateDescriptionFromLog(args):
2640 """Pulls out the commit log to use as a base for the CL description."""
2641 log_args = []
2642 if len(args) == 1 and not args[0].endswith('.'):
2643 log_args = [args[0] + '..']
2644 elif len(args) == 1 and args[0].endswith('...'):
2645 log_args = [args[0][:-1]]
2646 elif len(args) == 2:
2647 log_args = [args[0] + '..' + args[1]]
2648 else:
2649 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002650 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002651
2652
thestig@chromium.org44202a22014-03-11 19:22:18 +00002653def CMDlint(parser, args):
2654 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002655 parser.add_option('--filter', action='append', metavar='-x,+y',
2656 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002657 auth.add_auth_options(parser)
2658 options, args = parser.parse_args(args)
2659 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002660
2661 # Access to a protected member _XX of a client class
2662 # pylint: disable=W0212
2663 try:
2664 import cpplint
2665 import cpplint_chromium
2666 except ImportError:
2667 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2668 return 1
2669
2670 # Change the current working directory before calling lint so that it
2671 # shows the correct base.
2672 previous_cwd = os.getcwd()
2673 os.chdir(settings.GetRoot())
2674 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002675 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002676 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2677 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002678 if not files:
2679 print "Cannot lint an empty CL"
2680 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002681
2682 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002683 command = args + files
2684 if options.filter:
2685 command = ['--filter=' + ','.join(options.filter)] + command
2686 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002687
2688 white_regex = re.compile(settings.GetLintRegex())
2689 black_regex = re.compile(settings.GetLintIgnoreRegex())
2690 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2691 for filename in filenames:
2692 if white_regex.match(filename):
2693 if black_regex.match(filename):
2694 print "Ignoring file %s" % filename
2695 else:
2696 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2697 extra_check_functions)
2698 else:
2699 print "Skipping file %s" % filename
2700 finally:
2701 os.chdir(previous_cwd)
2702 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2703 if cpplint._cpplint_state.error_count != 0:
2704 return 1
2705 return 0
2706
2707
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002708def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002709 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002710 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002711 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002712 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002713 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002714 auth.add_auth_options(parser)
2715 options, args = parser.parse_args(args)
2716 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717
sbc@chromium.org71437c02015-04-09 19:29:40 +00002718 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002719 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002720 return 1
2721
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002722 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002723 if args:
2724 base_branch = args[0]
2725 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002726 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002727 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002728
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002729 cl.RunHook(
2730 committing=not options.upload,
2731 may_prompt=False,
2732 verbose=options.verbose,
2733 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002734 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002735
2736
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002737def AddChangeIdToCommitMessage(options, args):
2738 """Re-commits using the current message, assumes the commit hook is in
2739 place.
2740 """
2741 log_desc = options.message or CreateDescriptionFromLog(args)
2742 git_command = ['commit', '--amend', '-m', log_desc]
2743 RunGit(git_command)
2744 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002745 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002746 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002747 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002748 else:
2749 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2750
2751
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002752def GenerateGerritChangeId(message):
2753 """Returns Ixxxxxx...xxx change id.
2754
2755 Works the same way as
2756 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2757 but can be called on demand on all platforms.
2758
2759 The basic idea is to generate git hash of a state of the tree, original commit
2760 message, author/committer info and timestamps.
2761 """
2762 lines = []
2763 tree_hash = RunGitSilent(['write-tree'])
2764 lines.append('tree %s' % tree_hash.strip())
2765 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2766 if code == 0:
2767 lines.append('parent %s' % parent.strip())
2768 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2769 lines.append('author %s' % author.strip())
2770 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2771 lines.append('committer %s' % committer.strip())
2772 lines.append('')
2773 # Note: Gerrit's commit-hook actually cleans message of some lines and
2774 # whitespace. This code is not doing this, but it clearly won't decrease
2775 # entropy.
2776 lines.append(message)
2777 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2778 stdin='\n'.join(lines))
2779 return 'I%s' % change_hash.strip()
2780
2781
piman@chromium.org336f9122014-09-04 02:16:55 +00002782def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002783 """upload the current branch to gerrit."""
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002784 # TODO(tandrii): refactor this to be a method of _GerritChangelistImpl,
2785 # to avoid private members accessors below.
2786
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002787 # We assume the remote called "origin" is the one we want.
2788 # It is probably not worthwhile to support different workflows.
2789 gerrit_remote = 'origin'
2790
luqui@chromium.org609f3952015-05-04 22:47:04 +00002791 remote, remote_branch = cl.GetRemoteBranch()
2792 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2793 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002794
andybons@chromium.org962f9462016-02-03 20:00:42 +00002795 if options.title:
2796 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002798
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002799 if options.squash:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002800 if not cl.GetIssue():
2801 # TODO(tandrii): deperecate this after 2016Q2.
2802 # Backwards compatibility with shadow branch, which used to contain
2803 # change-id for a given branch, using which we can fetch actual issue
2804 # number and set it as the property of the branch, which is the new way.
2805 message = RunGitSilent(['show', '--format=%B', '-s',
2806 'refs/heads/git_cl_uploads/%s' % cl.GetBranch()])
2807 if message:
2808 change_ids = git_footers.get_footer_change_id(message.strip())
2809 if change_ids and len(change_ids) == 1:
2810 details = gerrit_util.GetChangeDetail(
2811 cl._codereview_impl._GetGerritHost(), change_ids[0])
2812 if details:
2813 print('WARNING: found old upload in branch git_cl_uploads/%s '
2814 'corresponding to issue %s' %
2815 (cl.GetBranch(), details['_number']))
2816 cl.SetIssue(details['_number'])
2817 if not cl.GetIssue():
2818 DieWithError(
2819 '\n' # For readability of the blob below.
2820 'Found old upload in branch git_cl_uploads/%s, '
2821 'but failed to find corresponding Gerrit issue.\n'
2822 'If you know the issue number, set it manually first:\n'
2823 ' git cl issue 123456\n'
2824 'If you intended to upload this CL as new issue, '
2825 'just delete or rename the old upload branch:\n'
2826 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2827 'After that, please run git cl upload again.' %
2828 tuple([cl.GetBranch()] * 3))
2829 # End of backwards compatability.
2830
2831 if cl.GetIssue():
2832 # Try to get the message from a previous upload.
2833 message = cl.GetDescription()
2834 if not message:
2835 DieWithError(
2836 'failed to fetch description from current Gerrit issue %d\n'
2837 '%s' % (cl.GetIssue(), cl.GetIssueURL()))
2838 change_id = cl._codereview_impl._GetChangeDetail([])['change_id']
2839 while True:
2840 footer_change_ids = git_footers.get_footer_change_id(message)
2841 if footer_change_ids == [change_id]:
2842 break
2843 if not footer_change_ids:
2844 message = git_footers.add_footer_change_id(message, change_id)
2845 print('WARNING: appended missing Change-Id to issue description')
2846 continue
2847 # There is already a valid footer but with different or several ids.
2848 # Doing this automatically is non-trivial as we don't want to lose
2849 # existing other footers, yet we want to append just 1 desired
2850 # Change-Id. Thus, just create a new footer, but let user verify the new
2851 # description.
2852 message = '%s\n\nChange-Id: %s' % (message, change_id)
2853 print(
2854 'WARNING: issue %s has Change-Id footer(s):\n'
2855 ' %s\n'
2856 'but issue has Change-Id %s, according to Gerrit.\n'
2857 'Please, check the proposed correction to the description, '
2858 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2859 % (cl.GetIssue(), '\n '.join(footer_change_ids), change_id,
2860 change_id))
2861 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2862 if not options.force:
2863 change_desc = ChangeDescription(message)
2864 change_desc.prompt()
2865 message = change_desc.description
2866 if not message:
2867 DieWithError("Description is empty. Aborting...")
2868 # Continue the while loop.
2869 # Sanity check of this code - we should end up with proper message footer.
2870 assert [change_id] == git_footers.get_footer_change_id(message)
2871 change_desc = ChangeDescription(message)
2872 else:
2873 change_desc = ChangeDescription(
2874 options.message or CreateDescriptionFromLog(args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002875 if not options.force:
2876 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002877 if not change_desc.description:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002878 DieWithError("Description is empty. Aborting...")
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002879 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002880 change_ids = git_footers.get_footer_change_id(message)
2881 if len(change_ids) > 1:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002882 DieWithError('too many Change-Id footers, at most 1 allowed.')
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002883 if not change_ids:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002884 # Generate the Change-Id automatically.
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002885 message = git_footers.add_footer_change_id(
2886 message, GenerateGerritChangeId(message))
2887 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002888 change_ids = git_footers.get_footer_change_id(message)
2889 assert len(change_ids) == 1
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002890 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002891
2892 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2893 if remote is '.':
2894 # If our upstream branch is local, we base our squashed commit on its
2895 # squashed version.
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002896 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2897 # Check the squashed hash of the parent.
2898 parent = RunGit(['config',
2899 'branch.%s.gerritsquashhash' % upstream_branch_name],
2900 error_ok=True).strip()
2901 # Verify that the upstream branch has been uploaded too, otherwise
2902 # Gerrit will create additional CLs when uploading.
2903 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2904 RunGitSilent(['rev-parse', parent + ':'])):
2905 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2906 DieWithError(
2907 'Upload upstream branch %s first.\n'
2908 'Note: maybe you\'ve uploaded it with --no-squash or with an old\n'
2909 ' version of depot_tools. If so, then re-upload it with:\n'
2910 ' git cl upload --squash\n' % upstream_branch_name)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002911 else:
2912 parent = cl.GetCommonAncestorWithUpstream()
2913
2914 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2915 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2916 '-m', message]).strip()
2917 else:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002918 change_desc = ChangeDescription(
2919 options.message or CreateDescriptionFromLog(args))
2920 if not change_desc.description:
2921 DieWithError("Description is empty. Aborting...")
2922
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002923 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002924 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002925 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002926 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002927 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002928 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002929
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002930 assert change_desc
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002931 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2932 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002933 if len(commits) > 1:
2934 print('WARNING: This will upload %d commits. Run the following command '
2935 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002936 print('git log %s..%s' % (parent, ref_to_push))
2937 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002938 'commit.')
2939 ask_for_data('About to upload; enter to confirm.')
2940
piman@chromium.org336f9122014-09-04 02:16:55 +00002941 if options.reviewers or options.tbr_owners:
2942 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002943
ukai@chromium.orge8077812012-02-03 03:41:46 +00002944 receive_options = []
2945 cc = cl.GetCCList().split(',')
2946 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002947 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002948 cc = filter(None, cc)
2949 if cc:
2950 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002951 if change_desc.get_reviewers():
2952 receive_options.extend(
2953 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002954
ukai@chromium.orge8077812012-02-03 03:41:46 +00002955 git_command = ['push']
2956 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002957 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002958 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002959 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002960 push_stdout = gclient_utils.CheckCallAndFilter(
2961 ['git'] + git_command,
2962 print_stdout=True,
2963 # Flush after every line: useful for seeing progress when running as
2964 # recipe.
2965 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002966
2967 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002968 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2969 change_numbers = [m.group(1)
2970 for m in map(regex.match, push_stdout.splitlines())
2971 if m]
2972 if len(change_numbers) != 1:
2973 DieWithError(
2974 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2975 'Change-Id: %s') % (len(change_numbers), change_id))
2976 cl.SetIssue(change_numbers[0])
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002977 RunGit(['config', 'branch.%s.gerritsquashhash' % cl.GetBranch(),
2978 ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002979 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002981
wittman@chromium.org455dc922015-01-26 20:15:50 +00002982def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2983 """Computes the remote branch ref to use for the CL.
2984
2985 Args:
2986 remote (str): The git remote for the CL.
2987 remote_branch (str): The git remote branch for the CL.
2988 target_branch (str): The target branch specified by the user.
2989 pending_prefix (str): The pending prefix from the settings.
2990 """
2991 if not (remote and remote_branch):
2992 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002993
wittman@chromium.org455dc922015-01-26 20:15:50 +00002994 if target_branch:
2995 # Cannonicalize branch references to the equivalent local full symbolic
2996 # refs, which are then translated into the remote full symbolic refs
2997 # below.
2998 if '/' not in target_branch:
2999 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3000 else:
3001 prefix_replacements = (
3002 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3003 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3004 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3005 )
3006 match = None
3007 for regex, replacement in prefix_replacements:
3008 match = re.search(regex, target_branch)
3009 if match:
3010 remote_branch = target_branch.replace(match.group(0), replacement)
3011 break
3012 if not match:
3013 # This is a branch path but not one we recognize; use as-is.
3014 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003015 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3016 # Handle the refs that need to land in different refs.
3017 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003018
wittman@chromium.org455dc922015-01-26 20:15:50 +00003019 # Create the true path to the remote branch.
3020 # Does the following translation:
3021 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3022 # * refs/remotes/origin/master -> refs/heads/master
3023 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3024 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3025 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3026 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3027 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3028 'refs/heads/')
3029 elif remote_branch.startswith('refs/remotes/branch-heads'):
3030 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3031 # If a pending prefix exists then replace refs/ with it.
3032 if pending_prefix:
3033 remote_branch = remote_branch.replace('refs/', pending_prefix)
3034 return remote_branch
3035
3036
piman@chromium.org336f9122014-09-04 02:16:55 +00003037def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003038 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003039 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003040 upload_args.extend(['--server', cl.GetCodereviewServer()])
3041 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
3042 upload_args.extend(auth.auth_config_to_command_options(
3043 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003044 if options.emulate_svn_auto_props:
3045 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003046
3047 change_desc = None
3048
pgervais@chromium.org91141372014-01-09 23:27:20 +00003049 if options.email is not None:
3050 upload_args.extend(['--email', options.email])
3051
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003052 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003053 if options.title:
3054 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00003055 if options.message:
3056 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00003057 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003058 print ("This branch is associated with issue %s. "
3059 "Adding patch to that issue." % cl.GetIssue())
3060 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003061 if options.title:
3062 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00003063 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003064 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00003065 if options.reviewers or options.tbr_owners:
3066 change_desc.update_reviewers(options.reviewers,
3067 options.tbr_owners,
3068 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003069 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003070 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003071
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003072 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003073 print "Description is empty; aborting."
3074 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003075
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003076 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077 if change_desc.get_reviewers():
3078 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00003079 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003080 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00003081 DieWithError("Must specify reviewers to send email.")
3082 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00003083
3084 # We check this before applying rietveld.private assuming that in
3085 # rietveld.cc only addresses which we can send private CLs to are listed
3086 # if rietveld.private is set, and so we should ignore rietveld.cc only when
3087 # --private is specified explicitly on the command line.
3088 if options.private:
3089 logging.warn('rietveld.cc is ignored since private flag is specified. '
3090 'You need to review and add them manually if necessary.')
3091 cc = cl.GetCCListWithoutDefault()
3092 else:
3093 cc = cl.GetCCList()
3094 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00003095 if cc:
3096 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003098 if options.private or settings.GetDefaultPrivateFlag() == "True":
3099 upload_args.append('--private')
3100
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003101 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00003102 if not options.find_copies:
3103 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003105 # Include the upstream repo's URL in the change -- this is useful for
3106 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003107 remote_url = cl.GetGitBaseUrlFromConfig()
3108 if not remote_url:
3109 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003110 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003111 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00003112 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
3113 remote_url = (cl.GetRemoteUrl() + '@'
3114 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003115 if remote_url:
3116 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00003117 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00003118 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
3119 settings.GetPendingRefPrefix())
3120 if target_ref:
3121 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003123 # Look for dependent patchsets. See crbug.com/480453 for more details.
3124 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3125 upstream_branch = ShortBranchName(upstream_branch)
3126 if remote is '.':
3127 # A local branch is being tracked.
3128 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00003129 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003130 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003131 print ('Skipping dependency patchset upload because git config '
3132 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003133 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003134 else:
3135 auth_config = auth.extract_auth_config_from_options(options)
3136 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
3137 branch_cl_issue_url = branch_cl.GetIssueURL()
3138 branch_cl_issue = branch_cl.GetIssue()
3139 branch_cl_patchset = branch_cl.GetPatchset()
3140 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
3141 upload_args.extend(
3142 ['--depends_on_patchset', '%s:%s' % (
3143 branch_cl_issue, branch_cl_patchset)])
3144 print
3145 print ('The current branch (%s) is tracking a local branch (%s) with '
3146 'an associated CL.') % (cl.GetBranch(), local_branch)
3147 print 'Adding %s/#ps%s as a dependency patchset.' % (
3148 branch_cl_issue_url, branch_cl_patchset)
3149 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003150
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003151 project = settings.GetProject()
3152 if project:
3153 upload_args.extend(['--project', project])
3154
rmistry@google.comef966222015-04-07 11:15:01 +00003155 if options.cq_dry_run:
3156 upload_args.extend(['--cq_dry_run'])
3157
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003158 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00003159 upload_args = ['upload'] + upload_args + args
3160 logging.info('upload.RealMain(%s)', upload_args)
3161 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00003162 issue = int(issue)
3163 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00003164 except KeyboardInterrupt:
3165 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003166 except:
3167 # If we got an exception after the user typed a description for their
3168 # change, back up the description before re-raising.
3169 if change_desc:
3170 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
3171 print '\nGot exception while uploading -- saving description to %s\n' \
3172 % backup_path
3173 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003174 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003175 backup_file.close()
3176 raise
3177
3178 if not cl.GetIssue():
3179 cl.SetIssue(issue)
3180 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003181
3182 if options.use_commit_queue:
3183 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003184 return 0
3185
3186
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003187def cleanup_list(l):
3188 """Fixes a list so that comma separated items are put as individual items.
3189
3190 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3191 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3192 """
3193 items = sum((i.split(',') for i in l), [])
3194 stripped_items = (i.strip() for i in items)
3195 return sorted(filter(None, stripped_items))
3196
3197
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003198@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003199def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003200 """Uploads the current changelist to codereview.
3201
3202 Can skip dependency patchset uploads for a branch by running:
3203 git config branch.branch_name.skip-deps-uploads True
3204 To unset run:
3205 git config --unset branch.branch_name.skip-deps-uploads
3206 Can also set the above globally by using the --global flag.
3207 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003208 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3209 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003210 parser.add_option('--bypass-watchlists', action='store_true',
3211 dest='bypass_watchlists',
3212 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003213 parser.add_option('-f', action='store_true', dest='force',
3214 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003215 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003216 parser.add_option('-t', dest='title',
3217 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003218 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003219 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003220 help='reviewer email addresses')
3221 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003222 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003223 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003224 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003225 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003226 parser.add_option('--emulate_svn_auto_props',
3227 '--emulate-svn-auto-props',
3228 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003229 dest="emulate_svn_auto_props",
3230 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003231 parser.add_option('-c', '--use-commit-queue', action='store_true',
3232 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003233 parser.add_option('--private', action='store_true',
3234 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003235 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003236 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003237 metavar='TARGET',
3238 help='Apply CL to remote ref TARGET. ' +
3239 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003240 parser.add_option('--squash', action='store_true',
3241 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003242 parser.add_option('--no-squash', action='store_true',
3243 help='Don\'t squash multiple commits into one ' +
3244 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003245 parser.add_option('--email', default=None,
3246 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003247 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3248 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003249 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3250 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003251 help='Send the patchset to do a CQ dry run right after '
3252 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003253 parser.add_option('--dependencies', action='store_true',
3254 help='Uploads CLs of all the local branches that depend on '
3255 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003256
rmistry@google.com2dd99862015-06-22 12:22:18 +00003257 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003258 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003259 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003260 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003261 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003262
sbc@chromium.org71437c02015-04-09 19:29:40 +00003263 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003264 return 1
3265
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003266 options.reviewers = cleanup_list(options.reviewers)
3267 options.cc = cleanup_list(options.cc)
3268
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003269 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3270 settings.GetIsGerrit()
3271
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003272 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003273 if args:
3274 # TODO(ukai): is it ok for gerrit case?
3275 base_branch = args[0]
3276 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003277 if cl.GetBranch() is None:
3278 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3279
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003280 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003281 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003282 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003283
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003284 # Make sure authenticated to Rietveld before running expensive hooks. It is
3285 # a fast, best efforts check. Rietveld still can reject the authentication
3286 # during the actual upload.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003287 if not cl.IsGerrit() and auth_config.use_oauth2:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003288 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003289 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003290 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003291 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003292
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003293 # Apply watchlists on upload.
3294 change = cl.GetChange(base_branch, None)
3295 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3296 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003297 if not options.bypass_watchlists:
3298 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003299
ukai@chromium.orge8077812012-02-03 03:41:46 +00003300 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003301 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003302 # Set the reviewer list now so that presubmit checks can access it.
3303 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003304 change_description.update_reviewers(options.reviewers,
3305 options.tbr_owners,
3306 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003307 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003308 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003309 may_prompt=not options.force,
3310 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003311 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003312 if not hook_results.should_continue():
3313 return 1
3314 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003315 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003316
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003317 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003318 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003319 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003320 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003321 print ('The last upload made from this repository was patchset #%d but '
3322 'the most recent patchset on the server is #%d.'
3323 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003324 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3325 'from another machine or branch the patch you\'re uploading now '
3326 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003327 ask_for_data('About to upload; enter to confirm.')
3328
iannucci@chromium.org79540052012-10-19 23:15:26 +00003329 print_stats(options.similarity, options.find_copies, args)
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003330 if cl.IsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003331 if options.squash and options.no_squash:
3332 DieWithError('Can only use one of --squash or --no-squash')
3333
3334 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3335 not options.no_squash)
3336
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00003337 ret = GerritUpload(options, args, cl, change)
3338 else:
3339 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003340 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003341 git_set_branch_value('last-upload-hash',
3342 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003343 # Run post upload hooks, if specified.
3344 if settings.GetRunPostUploadHook():
3345 presubmit_support.DoPostUploadExecuter(
3346 change,
3347 cl,
3348 settings.GetRoot(),
3349 options.verbose,
3350 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003351
rmistry@google.com2dd99862015-06-22 12:22:18 +00003352 # Upload all dependencies if specified.
3353 if options.dependencies:
3354 print
3355 print '--dependencies has been specified.'
3356 print 'All dependent local branches will be re-uploaded.'
3357 print
3358 # Remove the dependencies flag from args so that we do not end up in a
3359 # loop.
3360 orig_args.remove('--dependencies')
3361 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003362 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003363
3364
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003365def IsSubmoduleMergeCommit(ref):
3366 # When submodules are added to the repo, we expect there to be a single
3367 # non-git-svn merge commit at remote HEAD with a signature comment.
3368 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003369 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003370 return RunGit(cmd) != ''
3371
3372
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003373def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003374 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003375
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003376 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3377 upstream and closes the issue automatically and atomically.
3378
3379 Otherwise (in case of Rietveld):
3380 Squashes branch into a single commit.
3381 Updates changelog with metadata (e.g. pointer to review).
3382 Pushes/dcommits the code upstream.
3383 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003384 """
3385 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3386 help='bypass upload presubmit hook')
3387 parser.add_option('-m', dest='message',
3388 help="override review description")
3389 parser.add_option('-f', action='store_true', dest='force',
3390 help="force yes to questions (don't prompt)")
3391 parser.add_option('-c', dest='contributor',
3392 help="external contributor for patch (appended to " +
3393 "description and used as author for git). Should be " +
3394 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003395 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003396 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003397 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003398 auth_config = auth.extract_auth_config_from_options(options)
3399
3400 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003401
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003402 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3403 if cl.IsGerrit():
3404 if options.message:
3405 # This could be implemented, but it requires sending a new patch to
3406 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3407 # Besides, Gerrit has the ability to change the commit message on submit
3408 # automatically, thus there is no need to support this option (so far?).
3409 parser.error('-m MESSAGE option is not supported for Gerrit.')
3410 if options.contributor:
3411 parser.error(
3412 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3413 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3414 'the contributor\'s "name <email>". If you can\'t upload such a '
3415 'commit for review, contact your repository admin and request'
3416 '"Forge-Author" permission.')
3417 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3418 options.verbose)
3419
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003420 current = cl.GetBranch()
3421 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3422 if not settings.GetIsGitSvn() and remote == '.':
3423 print
3424 print 'Attempting to push branch %r into another local branch!' % current
3425 print
3426 print 'Either reparent this branch on top of origin/master:'
3427 print ' git reparent-branch --root'
3428 print
3429 print 'OR run `git rebase-update` if you think the parent branch is already'
3430 print 'committed.'
3431 print
3432 print ' Current parent: %r' % upstream_branch
3433 return 1
3434
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003435 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003436 # Default to merging against our best guess of the upstream branch.
3437 args = [cl.GetUpstreamBranch()]
3438
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003439 if options.contributor:
3440 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3441 print "Please provide contibutor as 'First Last <email@example.com>'"
3442 return 1
3443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003444 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003445 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003446
sbc@chromium.org71437c02015-04-09 19:29:40 +00003447 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003448 return 1
3449
3450 # This rev-list syntax means "show all commits not in my branch that
3451 # are in base_branch".
3452 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3453 base_branch]).splitlines()
3454 if upstream_commits:
3455 print ('Base branch "%s" has %d commits '
3456 'not in this branch.' % (base_branch, len(upstream_commits)))
3457 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3458 return 1
3459
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003460 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003461 svn_head = None
3462 if cmd == 'dcommit' or base_has_submodules:
3463 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3464 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003465
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003466 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003467 # If the base_head is a submodule merge commit, the first parent of the
3468 # base_head should be a git-svn commit, which is what we're interested in.
3469 base_svn_head = base_branch
3470 if base_has_submodules:
3471 base_svn_head += '^1'
3472
3473 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003474 if extra_commits:
3475 print ('This branch has %d additional commits not upstreamed yet.'
3476 % len(extra_commits.splitlines()))
3477 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3478 'before attempting to %s.' % (base_branch, cmd))
3479 return 1
3480
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003481 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003482 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003483 author = None
3484 if options.contributor:
3485 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003486 hook_results = cl.RunHook(
3487 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003488 may_prompt=not options.force,
3489 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003490 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003491 if not hook_results.should_continue():
3492 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003493
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003494 # Check the tree status if the tree status URL is set.
3495 status = GetTreeStatus()
3496 if 'closed' == status:
3497 print('The tree is closed. Please wait for it to reopen. Use '
3498 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3499 return 1
3500 elif 'unknown' == status:
3501 print('Unable to determine tree status. Please verify manually and '
3502 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3503 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003504
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003505 change_desc = ChangeDescription(options.message)
3506 if not change_desc.description and cl.GetIssue():
3507 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003508
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003509 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003510 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003511 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003512 else:
3513 print 'No description set.'
3514 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3515 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003516
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003517 # Keep a separate copy for the commit message, because the commit message
3518 # contains the link to the Rietveld issue, while the Rietveld message contains
3519 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003520 # Keep a separate copy for the commit message.
3521 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003522 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003523
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003524 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003525 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003526 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003527 # after it. Add a period on a new line to circumvent this. Also add a space
3528 # before the period to make sure that Gitiles continues to correctly resolve
3529 # the URL.
3530 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003531 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003532 commit_desc.append_footer('Patch from %s.' % options.contributor)
3533
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003534 print('Description:')
3535 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003536
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003537 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003538 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003539 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003540
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003541 # We want to squash all this branch's commits into one commit with the proper
3542 # description. We do this by doing a "reset --soft" to the base branch (which
3543 # keeps the working copy the same), then dcommitting that. If origin/master
3544 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3545 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003547 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3548 # Delete the branches if they exist.
3549 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3550 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3551 result = RunGitWithCode(showref_cmd)
3552 if result[0] == 0:
3553 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003554
3555 # We might be in a directory that's present in this branch but not in the
3556 # trunk. Move up to the top of the tree so that git commands that expect a
3557 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003558 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003559 if rel_base_path:
3560 os.chdir(rel_base_path)
3561
3562 # Stuff our change into the merge branch.
3563 # We wrap in a try...finally block so if anything goes wrong,
3564 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003565 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003566 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003567 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003568 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003569 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003570 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003571 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003572 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003573 RunGit(
3574 [
3575 'commit', '--author', options.contributor,
3576 '-m', commit_desc.description,
3577 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003578 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003579 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003580 if base_has_submodules:
3581 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3582 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3583 RunGit(['checkout', CHERRY_PICK_BRANCH])
3584 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003585 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003586 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003587 mirror = settings.GetGitMirror(remote)
3588 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003589 pending_prefix = settings.GetPendingRefPrefix()
3590 if not pending_prefix or branch.startswith(pending_prefix):
3591 # If not using refs/pending/heads/* at all, or target ref is already set
3592 # to pending, then push to the target ref directly.
3593 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003594 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003595 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003596 else:
3597 # Cherry-pick the change on top of pending ref and then push it.
3598 assert branch.startswith('refs/'), branch
3599 assert pending_prefix[-1] == '/', pending_prefix
3600 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003601 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003602 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003603 if retcode == 0:
3604 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003605 else:
3606 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003607 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003608 'svn', 'dcommit',
3609 '-C%s' % options.similarity,
3610 '--no-rebase', '--rmdir',
3611 ]
3612 if settings.GetForceHttpsCommitUrl():
3613 # Allow forcing https commit URLs for some projects that don't allow
3614 # committing to http URLs (like Google Code).
3615 remote_url = cl.GetGitSvnRemoteUrl()
3616 if urlparse.urlparse(remote_url).scheme == 'http':
3617 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003618 cmd_args.append('--commit-url=%s' % remote_url)
3619 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003620 if 'Committed r' in output:
3621 revision = re.match(
3622 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3623 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624 finally:
3625 # And then swap back to the original branch and clean up.
3626 RunGit(['checkout', '-q', cl.GetBranch()])
3627 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003628 if base_has_submodules:
3629 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003630
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003631 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003632 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003633 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003634
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003635 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003636 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003637 try:
3638 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3639 # We set pushed_to_pending to False, since it made it all the way to the
3640 # real ref.
3641 pushed_to_pending = False
3642 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003643 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003646 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003648 if not to_pending:
3649 if viewvc_url and revision:
3650 change_desc.append_footer(
3651 'Committed: %s%s' % (viewvc_url, revision))
3652 elif revision:
3653 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003654 print ('Closing issue '
3655 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003656 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003657 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003658 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003659 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003660 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003661 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003662 if options.bypass_hooks:
3663 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3664 else:
3665 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003666 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003667 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003668
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003669 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003670 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3671 print 'The commit is in the pending queue (%s).' % pending_ref
3672 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003673 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003674 'footer.' % branch)
3675
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003676 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3677 if os.path.isfile(hook):
3678 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003679
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003680 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681
3682
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003683def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3684 print
3685 print 'Waiting for commit to be landed on %s...' % real_ref
3686 print '(If you are impatient, you may Ctrl-C once without harm)'
3687 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3688 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003689 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003690
3691 loop = 0
3692 while True:
3693 sys.stdout.write('fetching (%d)... \r' % loop)
3694 sys.stdout.flush()
3695 loop += 1
3696
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003697 if mirror:
3698 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003699 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3700 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3701 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3702 for commit in commits.splitlines():
3703 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3704 print 'Found commit on %s' % real_ref
3705 return commit
3706
3707 current_rev = to_rev
3708
3709
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003710def PushToGitPending(remote, pending_ref, upstream_ref):
3711 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3712
3713 Returns:
3714 (retcode of last operation, output log of last operation).
3715 """
3716 assert pending_ref.startswith('refs/'), pending_ref
3717 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3718 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3719 code = 0
3720 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003721 max_attempts = 3
3722 attempts_left = max_attempts
3723 while attempts_left:
3724 if attempts_left != max_attempts:
3725 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3726 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003727
3728 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003729 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003730 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003731 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003732 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003733 print 'Fetch failed with exit code %d.' % code
3734 if out.strip():
3735 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003736 continue
3737
3738 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003739 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003740 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003741 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003742 if code:
3743 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003744 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3745 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003746 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3747 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003748 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003749 return code, out
3750
3751 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003752 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003753 code, out = RunGitWithCode(
3754 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3755 if code == 0:
3756 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003757 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003758 return code, out
3759
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003760 print 'Push failed with exit code %d.' % code
3761 if out.strip():
3762 print out.strip()
3763 if IsFatalPushFailure(out):
3764 print (
3765 'Fatal push error. Make sure your .netrc credentials and git '
3766 'user.email are correct and you have push access to the repo.')
3767 return code, out
3768
3769 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003770 return code, out
3771
3772
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003773def IsFatalPushFailure(push_stdout):
3774 """True if retrying push won't help."""
3775 return '(prohibited by Gerrit)' in push_stdout
3776
3777
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003778@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003779def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003780 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003781 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003782 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003783 # If it looks like previous commits were mirrored with git-svn.
3784 message = """This repository appears to be a git-svn mirror, but no
3785upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3786 else:
3787 message = """This doesn't appear to be an SVN repository.
3788If your project has a true, writeable git repository, you probably want to run
3789'git cl land' instead.
3790If your project has a git mirror of an upstream SVN master, you probably need
3791to run 'git svn init'.
3792
3793Using the wrong command might cause your commit to appear to succeed, and the
3794review to be closed, without actually landing upstream. If you choose to
3795proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003796 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003797 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003798 return SendUpstream(parser, args, 'dcommit')
3799
3800
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003801@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003802def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003803 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003804 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003805 print('This appears to be an SVN repository.')
3806 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003807 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003808 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003809 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003810
3811
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003812@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003814 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003815 parser.add_option('-b', dest='newbranch',
3816 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003817 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003818 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003819 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3820 help='Change to the directory DIR immediately, '
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003821 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003822 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003823 help='failed patches spew .rej files rather than '
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003824 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003825 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003826 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003827
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003828
3829 group = optparse.OptionGroup(
3830 parser,
3831 'Options for continuing work on the current issue uploaded from a '
3832 'different clone (e.g. different machine). Must be used independently '
3833 'from the other options. No issue number should be specified, and the '
3834 'branch must have an issue number associated with it')
3835 group.add_option('--reapply', action='store_true', dest='reapply',
3836 help='Reset the branch and reapply the issue.\n'
3837 'CAUTION: This will undo any local changes in this '
3838 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003839
3840 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003841 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003842 parser.add_option_group(group)
3843
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003844 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003845 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003846 auth_config = auth.extract_auth_config_from_options(options)
3847
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003848 cl = Changelist(auth_config=auth_config)
3849
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003850 issue_arg = None
3851 if options.reapply :
3852 if len(args) > 0:
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003853 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003854
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003855 issue_arg = cl.GetIssue()
3856 upstream = cl.GetUpstreamBranch()
3857 if upstream == None:
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003858 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003859
3860 RunGit(['reset', '--hard', upstream])
3861 if options.pull:
3862 RunGit(['pull'])
3863 else:
3864 if len(args) != 1:
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003865 parser.error('Must specify issue number or url')
3866 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003867
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003868 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003869 parser.print_help()
3870 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003871
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003872 if cl.IsGerrit():
3873 if options.reject:
3874 parser.error('--reject is not supported with Gerrit codereview.')
3875 if options.nocommit:
3876 parser.error('--nocommit is not supported with Gerrit codereview.')
3877 if options.directory:
3878 parser.error('--directory is not supported with Gerrit codereview.')
3879
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003880 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003881 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003882 return 1
3883
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003884 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003885 if options.reapply:
3886 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003887 if options.force:
3888 RunGit(['branch', '-D', options.newbranch],
3889 stderr=subprocess2.PIPE, error_ok=True)
3890 RunGit(['checkout', '-b', options.newbranch,
3891 Changelist().GetUpstreamBranch()])
3892
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00003893 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3894 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003895
3896
3897def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003898 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003899 # Provide a wrapper for git svn rebase to help avoid accidental
3900 # git svn dcommit.
3901 # It's the only command that doesn't use parser at all since we just defer
3902 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003903
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003904 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905
3906
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003907def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003908 """Fetches the tree status and returns either 'open', 'closed',
3909 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003910 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003911 if url:
3912 status = urllib2.urlopen(url).read().lower()
3913 if status.find('closed') != -1 or status == '0':
3914 return 'closed'
3915 elif status.find('open') != -1 or status == '1':
3916 return 'open'
3917 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003918 return 'unset'
3919
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003920
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003921def GetTreeStatusReason():
3922 """Fetches the tree status from a json url and returns the message
3923 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003924 url = settings.GetTreeStatusUrl()
3925 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926 connection = urllib2.urlopen(json_url)
3927 status = json.loads(connection.read())
3928 connection.close()
3929 return status['message']
3930
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003931
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003932def GetBuilderMaster(bot_list):
3933 """For a given builder, fetch the master from AE if available."""
3934 map_url = 'https://builders-map.appspot.com/'
3935 try:
3936 master_map = json.load(urllib2.urlopen(map_url))
3937 except urllib2.URLError as e:
3938 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3939 (map_url, e))
3940 except ValueError as e:
3941 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3942 if not master_map:
3943 return None, 'Failed to build master map.'
3944
3945 result_master = ''
3946 for bot in bot_list:
3947 builder = bot.split(':', 1)[0]
3948 master_list = master_map.get(builder, [])
3949 if not master_list:
3950 return None, ('No matching master for builder %s.' % builder)
3951 elif len(master_list) > 1:
3952 return None, ('The builder name %s exists in multiple masters %s.' %
3953 (builder, master_list))
3954 else:
3955 cur_master = master_list[0]
3956 if not result_master:
3957 result_master = cur_master
3958 elif result_master != cur_master:
3959 return None, 'The builders do not belong to the same master.'
3960 return result_master, None
3961
3962
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003963def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003964 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003965 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003966 status = GetTreeStatus()
3967 if 'unset' == status:
3968 print 'You must configure your tree status URL by running "git cl config".'
3969 return 2
3970
3971 print "The tree is %s" % status
3972 print
3973 print GetTreeStatusReason()
3974 if status != 'open':
3975 return 1
3976 return 0
3977
3978
maruel@chromium.org15192402012-09-06 12:38:29 +00003979def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003980 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003981 group = optparse.OptionGroup(parser, "Try job options")
3982 group.add_option(
3983 "-b", "--bot", action="append",
3984 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3985 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003986 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003987 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003988 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003989 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003990 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003991 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003992 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003993 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003994 "-r", "--revision",
3995 help="Revision to use for the try job; default: the "
3996 "revision will be determined by the try server; see "
3997 "its waterfall for more info")
3998 group.add_option(
3999 "-c", "--clobber", action="store_true", default=False,
4000 help="Force a clobber before building; e.g. don't do an "
4001 "incremental build")
4002 group.add_option(
4003 "--project",
4004 help="Override which project to use. Projects are defined "
4005 "server-side to define what default bot set to use")
4006 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004007 "-p", "--property", dest="properties", action="append", default=[],
4008 help="Specify generic properties in the form -p key1=value1 -p "
4009 "key2=value2 etc (buildbucket only). The value will be treated as "
4010 "json if decodable, or as string otherwise.")
4011 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004012 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004013 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004014 "--use-rietveld", action="store_true", default=False,
4015 help="Use Rietveld to trigger try jobs.")
4016 group.add_option(
4017 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4018 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004019 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004020 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004021 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004022 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004023
machenbach@chromium.org45453142015-09-15 08:45:22 +00004024 if options.use_rietveld and options.properties:
4025 parser.error('Properties can only be specified with buildbucket')
4026
4027 # Make sure that all properties are prop=value pairs.
4028 bad_params = [x for x in options.properties if '=' not in x]
4029 if bad_params:
4030 parser.error('Got properties with missing "=": %s' % bad_params)
4031
maruel@chromium.org15192402012-09-06 12:38:29 +00004032 if args:
4033 parser.error('Unknown arguments: %s' % args)
4034
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004035 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004036 if not cl.GetIssue():
4037 parser.error('Need to upload first')
4038
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004039 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004040 if props.get('closed'):
4041 parser.error('Cannot send tryjobs for a closed CL')
4042
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004043 if props.get('private'):
4044 parser.error('Cannot use trybots with private issue')
4045
maruel@chromium.org15192402012-09-06 12:38:29 +00004046 if not options.name:
4047 options.name = cl.GetBranch()
4048
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004049 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004050 options.master, err_msg = GetBuilderMaster(options.bot)
4051 if err_msg:
4052 parser.error('Tryserver master cannot be found because: %s\n'
4053 'Please manually specify the tryserver master'
4054 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004055
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004056 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004057 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004058 if not options.bot:
4059 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004060
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004061 # Get try masters from PRESUBMIT.py files.
4062 masters = presubmit_support.DoGetTryMasters(
4063 change,
4064 change.LocalPaths(),
4065 settings.GetRoot(),
4066 None,
4067 None,
4068 options.verbose,
4069 sys.stdout)
4070 if masters:
4071 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004072
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004073 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4074 options.bot = presubmit_support.DoGetTrySlaves(
4075 change,
4076 change.LocalPaths(),
4077 settings.GetRoot(),
4078 None,
4079 None,
4080 options.verbose,
4081 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004082
4083 if not options.bot:
4084 # Get try masters from cq.cfg if any.
4085 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4086 # location.
4087 cq_cfg = os.path.join(change.RepositoryRoot(),
4088 'infra', 'config', 'cq.cfg')
4089 if os.path.exists(cq_cfg):
4090 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004091 cq_masters = commit_queue.get_master_builder_map(
4092 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004093 for master, builders in cq_masters.iteritems():
4094 for builder in builders:
4095 # Skip presubmit builders, because these will fail without LGTM.
4096 if 'presubmit' not in builder.lower():
4097 masters.setdefault(master, {})[builder] = ['defaulttests']
4098 if masters:
4099 return masters
4100
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004101 if not options.bot:
4102 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004103
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004104 builders_and_tests = {}
4105 # TODO(machenbach): The old style command-line options don't support
4106 # multiple try masters yet.
4107 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4108 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4109
4110 for bot in old_style:
4111 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004112 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004113 elif ',' in bot:
4114 parser.error('Specify one bot per --bot flag')
4115 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004116 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004117
4118 for bot, tests in new_style:
4119 builders_and_tests.setdefault(bot, []).extend(tests)
4120
4121 # Return a master map with one master to be backwards compatible. The
4122 # master name defaults to an empty string, which will cause the master
4123 # not to be set on rietveld (deprecated).
4124 return {options.master: builders_and_tests}
4125
4126 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004127
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004128 for builders in masters.itervalues():
4129 if any('triggered' in b for b in builders):
4130 print >> sys.stderr, (
4131 'ERROR You are trying to send a job to a triggered bot. This type of'
4132 ' bot requires an\ninitial job from a parent (usually a builder). '
4133 'Instead send your job to the parent.\n'
4134 'Bot list: %s' % builders)
4135 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004136
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004137 patchset = cl.GetMostRecentPatchset()
4138 if patchset and patchset != cl.GetPatchset():
4139 print(
4140 '\nWARNING Mismatch between local config and server. Did a previous '
4141 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4142 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004143 if options.luci:
4144 trigger_luci_job(cl, masters, options)
4145 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004146 try:
4147 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4148 except BuildbucketResponseException as ex:
4149 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004150 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004151 except Exception as e:
4152 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4153 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4154 e, stacktrace)
4155 return 1
4156 else:
4157 try:
4158 cl.RpcServer().trigger_distributed_try_jobs(
4159 cl.GetIssue(), patchset, options.name, options.clobber,
4160 options.revision, masters)
4161 except urllib2.HTTPError as e:
4162 if e.code == 404:
4163 print('404 from rietveld; '
4164 'did you mean to use "git try" instead of "git cl try"?')
4165 return 1
4166 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004167
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004168 for (master, builders) in sorted(masters.iteritems()):
4169 if master:
4170 print 'Master: %s' % master
4171 length = max(len(builder) for builder in builders)
4172 for builder in sorted(builders):
4173 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004174 return 0
4175
4176
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004177def CMDtry_results(parser, args):
4178 group = optparse.OptionGroup(parser, "Try job results options")
4179 group.add_option(
4180 "-p", "--patchset", type=int, help="patchset number if not current.")
4181 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004182 "--print-master", action='store_true', help="print master name as well.")
4183 group.add_option(
4184 "--color", action='store_true', default=sys.stdout.isatty(),
4185 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004186 group.add_option(
4187 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4188 help="Host of buildbucket. The default host is %default.")
4189 parser.add_option_group(group)
4190 auth.add_auth_options(parser)
4191 options, args = parser.parse_args(args)
4192 if args:
4193 parser.error('Unrecognized args: %s' % ' '.join(args))
4194
4195 auth_config = auth.extract_auth_config_from_options(options)
4196 cl = Changelist(auth_config=auth_config)
4197 if not cl.GetIssue():
4198 parser.error('Need to upload first')
4199
4200 if not options.patchset:
4201 options.patchset = cl.GetMostRecentPatchset()
4202 if options.patchset and options.patchset != cl.GetPatchset():
4203 print(
4204 '\nWARNING Mismatch between local config and server. Did a previous '
4205 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4206 'Continuing using\npatchset %s.\n' % options.patchset)
4207 try:
4208 jobs = fetch_try_jobs(auth_config, cl, options)
4209 except BuildbucketResponseException as ex:
4210 print 'Buildbucket error: %s' % ex
4211 return 1
4212 except Exception as e:
4213 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4214 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4215 e, stacktrace)
4216 return 1
4217 print_tryjobs(options, jobs)
4218 return 0
4219
4220
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004221@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004223 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004224 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004225 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004226 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004228 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004229 if args:
4230 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004231 branch = cl.GetBranch()
4232 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004233 cl = Changelist()
4234 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004235
4236 # Clear configured merge-base, if there is one.
4237 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004238 else:
4239 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004240 return 0
4241
4242
thestig@chromium.org00858c82013-12-02 23:08:03 +00004243def CMDweb(parser, args):
4244 """Opens the current CL in the web browser."""
4245 _, args = parser.parse_args(args)
4246 if args:
4247 parser.error('Unrecognized args: %s' % ' '.join(args))
4248
4249 issue_url = Changelist().GetIssueURL()
4250 if not issue_url:
4251 print >> sys.stderr, 'ERROR No issue to open'
4252 return 1
4253
4254 webbrowser.open(issue_url)
4255 return 0
4256
4257
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004258def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004259 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004260 auth.add_auth_options(parser)
4261 options, args = parser.parse_args(args)
4262 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004263 if args:
4264 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004265 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004266 props = cl.GetIssueProperties()
4267 if props.get('private'):
4268 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004269 cl.SetFlag('commit', '1')
4270 return 0
4271
4272
groby@chromium.org411034a2013-02-26 15:12:01 +00004273def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004274 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004275 auth.add_auth_options(parser)
4276 options, args = parser.parse_args(args)
4277 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004278 if args:
4279 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004280 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004281 # Ensure there actually is an issue to close.
4282 cl.GetDescription()
4283 cl.CloseIssue()
4284 return 0
4285
4286
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004287def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004288 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004289 auth.add_auth_options(parser)
4290 options, args = parser.parse_args(args)
4291 auth_config = auth.extract_auth_config_from_options(options)
4292 if args:
4293 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004294
4295 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00004296 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004297 # Staged changes would be committed along with the patch from last
4298 # upload, hence counted toward the "last upload" side in the final
4299 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004300 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004301 return 1
4302
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004303 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004304 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004305 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004306 if not issue:
4307 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004308 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004309 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004310
4311 # Create a new branch based on the merge-base
4312 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
4313 try:
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00004314 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004315 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004316 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004317 return rtn
4318
wychen@chromium.org06928532015-02-03 02:11:29 +00004319 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004320 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004321 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004322 finally:
4323 RunGit(['checkout', '-q', branch])
4324 RunGit(['branch', '-D', TMP_BRANCH])
4325
4326 return 0
4327
4328
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004329def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004330 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004331 parser.add_option(
4332 '--no-color',
4333 action='store_true',
4334 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004335 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004336 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004337 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004338
4339 author = RunGit(['config', 'user.email']).strip() or None
4340
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004341 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004342
4343 if args:
4344 if len(args) > 1:
4345 parser.error('Unknown args')
4346 base_branch = args[0]
4347 else:
4348 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004349 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004350
4351 change = cl.GetChange(base_branch, None)
4352 return owners_finder.OwnersFinder(
4353 [f.LocalPath() for f in
4354 cl.GetChange(base_branch, None).AffectedFiles()],
4355 change.RepositoryRoot(), author,
4356 fopen=file, os_path=os.path, glob=glob.glob,
4357 disable_color=options.no_color).run()
4358
4359
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004360def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004361 """Generates a diff command."""
4362 # Generate diff for the current branch's changes.
4363 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4364 upstream_commit, '--' ]
4365
4366 if args:
4367 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004368 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004369 diff_cmd.append(arg)
4370 else:
4371 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004372
4373 return diff_cmd
4374
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004375def MatchingFileType(file_name, extensions):
4376 """Returns true if the file name ends with one of the given extensions."""
4377 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004378
enne@chromium.org555cfe42014-01-29 18:21:39 +00004379@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004380def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004381 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004382 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004383 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004384 parser.add_option('--full', action='store_true',
4385 help='Reformat the full content of all touched files')
4386 parser.add_option('--dry-run', action='store_true',
4387 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004388 parser.add_option('--python', action='store_true',
4389 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004390 parser.add_option('--diff', action='store_true',
4391 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004392 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004393
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004394 # git diff generates paths against the root of the repository. Change
4395 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004396 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004397 if rel_base_path:
4398 os.chdir(rel_base_path)
4399
digit@chromium.org29e47272013-05-17 17:01:46 +00004400 # Grab the merge-base commit, i.e. the upstream commit of the current
4401 # branch when it was created or the last time it was rebased. This is
4402 # to cover the case where the user may have called "git fetch origin",
4403 # moving the origin branch to a newer commit, but hasn't rebased yet.
4404 upstream_commit = None
4405 cl = Changelist()
4406 upstream_branch = cl.GetUpstreamBranch()
4407 if upstream_branch:
4408 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4409 upstream_commit = upstream_commit.strip()
4410
4411 if not upstream_commit:
4412 DieWithError('Could not find base commit for this branch. '
4413 'Are you in detached state?')
4414
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004415 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4416 diff_output = RunGit(changed_files_cmd)
4417 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004418 # Filter out files deleted by this CL
4419 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004420
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004421 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4422 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4423 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004424 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004425
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004426 top_dir = os.path.normpath(
4427 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4428
4429 # Locate the clang-format binary in the checkout
4430 try:
4431 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4432 except clang_format.NotFoundError, e:
4433 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004434
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004435 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4436 # formatted. This is used to block during the presubmit.
4437 return_value = 0
4438
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004439 if clang_diff_files:
4440 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004441 cmd = [clang_format_tool]
4442 if not opts.dry_run and not opts.diff:
4443 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004444 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004445 if opts.diff:
4446 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004447 else:
4448 env = os.environ.copy()
4449 env['PATH'] = str(os.path.dirname(clang_format_tool))
4450 try:
4451 script = clang_format.FindClangFormatScriptInChromiumTree(
4452 'clang-format-diff.py')
4453 except clang_format.NotFoundError, e:
4454 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004455
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004456 cmd = [sys.executable, script, '-p0']
4457 if not opts.dry_run and not opts.diff:
4458 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004459
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004460 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4461 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004462
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004463 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4464 if opts.diff:
4465 sys.stdout.write(stdout)
4466 if opts.dry_run and len(stdout) > 0:
4467 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004468
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004469 # Similar code to above, but using yapf on .py files rather than clang-format
4470 # on C/C++ files
4471 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004472 yapf_tool = gclient_utils.FindExecutable('yapf')
4473 if yapf_tool is None:
4474 DieWithError('yapf not found in PATH')
4475
4476 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004477 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004478 cmd = [yapf_tool]
4479 if not opts.dry_run and not opts.diff:
4480 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004481 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004482 if opts.diff:
4483 sys.stdout.write(stdout)
4484 else:
4485 # TODO(sbc): yapf --lines mode still has some issues.
4486 # https://github.com/google/yapf/issues/154
4487 DieWithError('--python currently only works with --full')
4488
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004489 # Dart's formatter does not have the nice property of only operating on
4490 # modified chunks, so hard code full.
4491 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004492 try:
4493 command = [dart_format.FindDartFmtToolInChromiumTree()]
4494 if not opts.dry_run and not opts.diff:
4495 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004496 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004497
ppi@chromium.org6593d932016-03-03 15:41:15 +00004498 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004499 if opts.dry_run and stdout:
4500 return_value = 2
4501 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004502 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4503 'found in this checkout. Files in other languages are still ' +
4504 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004505
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004506 # Format GN build files. Always run on full build files for canonical form.
4507 if gn_diff_files:
4508 cmd = ['gn', 'format']
4509 if not opts.dry_run and not opts.diff:
4510 cmd.append('--in-place')
4511 for gn_diff_file in gn_diff_files:
4512 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4513 if opts.diff:
4514 sys.stdout.write(stdout)
4515
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004516 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004517
4518
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004519@subcommand.usage('<codereview url or issue id>')
4520def CMDcheckout(parser, args):
4521 """Checks out a branch associated with a given Rietveld issue."""
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00004522 # TODO(tandrii): consider adding this for Gerrit?
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004523 _, args = parser.parse_args(args)
4524
4525 if len(args) != 1:
4526 parser.print_help()
4527 return 1
4528
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00004529 issue_arg = ParseIssueNumberArgument(args[0])
4530 if issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004531 parser.print_help()
4532 return 1
tandrii@chromium.orgcc0856a2016-04-01 19:03:12 +00004533 target_issue = issue_arg.issue
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004534
4535 key_and_issues = [x.split() for x in RunGit(
4536 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4537 .splitlines()]
4538 branches = []
4539 for key, issue in key_and_issues:
4540 if issue == target_issue:
4541 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4542
4543 if len(branches) == 0:
4544 print 'No branch found for issue %s.' % target_issue
4545 return 1
4546 if len(branches) == 1:
4547 RunGit(['checkout', branches[0]])
4548 else:
4549 print 'Multiple branches match issue %s:' % target_issue
4550 for i in range(len(branches)):
4551 print '%d: %s' % (i, branches[i])
4552 which = raw_input('Choose by index: ')
4553 try:
4554 RunGit(['checkout', branches[int(which)]])
4555 except (IndexError, ValueError):
4556 print 'Invalid selection, not checking out any branch.'
4557 return 1
4558
4559 return 0
4560
4561
maruel@chromium.org29404b52014-09-08 22:58:00 +00004562def CMDlol(parser, args):
4563 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004564 print zlib.decompress(base64.b64decode(
4565 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4566 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4567 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4568 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004569 return 0
4570
4571
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004572class OptionParser(optparse.OptionParser):
4573 """Creates the option parse and add --verbose support."""
4574 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004575 optparse.OptionParser.__init__(
4576 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004577 self.add_option(
4578 '-v', '--verbose', action='count', default=0,
4579 help='Use 2 times for more debugging info')
4580
4581 def parse_args(self, args=None, values=None):
4582 options, args = optparse.OptionParser.parse_args(self, args, values)
4583 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4584 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4585 return options, args
4586
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004587
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004588def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004589 if sys.hexversion < 0x02060000:
4590 print >> sys.stderr, (
4591 '\nYour python version %s is unsupported, please upgrade.\n' %
4592 sys.version.split(' ', 1)[0])
4593 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004594
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004595 # Reload settings.
4596 global settings
4597 settings = Settings()
4598
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004599 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004600 dispatcher = subcommand.CommandDispatcher(__name__)
4601 try:
4602 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004603 except auth.AuthenticationError as e:
4604 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004605 except urllib2.HTTPError, e:
4606 if e.code != 500:
4607 raise
4608 DieWithError(
4609 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4610 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004611 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004612
4613
4614if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004615 # These affect sys.stdout so do it outside of main() to simplify mocks in
4616 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004617 fix_encoding.fix_encoding()
iannucci@chromium.org0703ea22016-04-01 01:02:42 +00004618 colorama.init(wrap="TERM" not in os.environ)
sbc@chromium.org013731e2015-02-26 18:28:43 +00004619 try:
4620 sys.exit(main(sys.argv[1:]))
4621 except KeyboardInterrupt:
4622 sys.stderr.write('interrupted\n')
4623 sys.exit(1)