blob: 34c96c0581d41f6e5f7f7fa24ec775fd2e9d6935 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000838class _CQState(object):
839 """Enum for states of CL with respect to Commit Queue."""
840 NONE = 'none'
841 DRY_RUN = 'dry_run'
842 COMMIT = 'commit'
843
844 ALL_STATES = [NONE, DRY_RUN, COMMIT]
845
846
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000847class _ParsedIssueNumberArgument(object):
848 def __init__(self, issue=None, patchset=None, hostname=None):
849 self.issue = issue
850 self.patchset = patchset
851 self.hostname = hostname
852
853 @property
854 def valid(self):
855 return self.issue is not None
856
857
858class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
859 def __init__(self, *args, **kwargs):
860 self.patch_url = kwargs.pop('patch_url', None)
861 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
862
863
864def ParseIssueNumberArgument(arg):
865 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
866 fail_result = _ParsedIssueNumberArgument()
867
868 if arg.isdigit():
869 return _ParsedIssueNumberArgument(issue=int(arg))
870 if not arg.startswith('http'):
871 return fail_result
872 url = gclient_utils.UpgradeToHttps(arg)
873 try:
874 parsed_url = urlparse.urlparse(url)
875 except ValueError:
876 return fail_result
877 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
878 tmp = cls.ParseIssueURL(parsed_url)
879 if tmp is not None:
880 return tmp
881 return fail_result
882
883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000885 """Changelist works with one changelist in local branch.
886
887 Supports two codereview backends: Rietveld or Gerrit, selected at object
888 creation.
889
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000890 Notes:
891 * Not safe for concurrent multi-{thread,process} use.
892 * Caches values from current branch. Therefore, re-use after branch change
893 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000894 """
895
896 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
897 """Create a new ChangeList instance.
898
899 If issue is given, the codereview must be given too.
900
901 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
902 Otherwise, it's decided based on current configuration of the local branch,
903 with default being 'rietveld' for backwards compatibility.
904 See _load_codereview_impl for more details.
905
906 **kwargs will be passed directly to codereview implementation.
907 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000909 global settings
910 if not settings:
911 # Happens when git_cl.py is used as a utility library.
912 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000913
914 if issue:
915 assert codereview, 'codereview must be known, if issue is known'
916
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917 self.branchref = branchref
918 if self.branchref:
919 self.branch = ShortBranchName(self.branchref)
920 else:
921 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000923 self.lookedup_issue = False
924 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 self.has_description = False
926 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000927 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000929 self.cc = None
930 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000931 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000932
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000934 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000935 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000936 assert self._codereview_impl
937 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000938
939 def _load_codereview_impl(self, codereview=None, **kwargs):
940 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000941 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
942 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
943 self._codereview = codereview
944 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 return
946
947 # Automatic selection based on issue number set for a current branch.
948 # Rietveld takes precedence over Gerrit.
949 assert not self.issue
950 # Whether we find issue or not, we are doing the lookup.
951 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000952 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000953 setting = cls.IssueSetting(self.GetBranch())
954 issue = RunGit(['config', setting], error_ok=True).strip()
955 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000956 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000957 self._codereview_impl = cls(self, **kwargs)
958 self.issue = int(issue)
959 return
960
961 # No issue is set for this branch, so decide based on repo-wide settings.
962 return self._load_codereview_impl(
963 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
964 **kwargs)
965
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000966 def IsGerrit(self):
967 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000968
969 def GetCCList(self):
970 """Return the users cc'd on this CL.
971
972 Return is a string suitable for passing to gcl with the --cc flag.
973 """
974 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000975 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000976 more_cc = ','.join(self.watchers)
977 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
978 return self.cc
979
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000980 def GetCCListWithoutDefault(self):
981 """Return the users cc'd on this CL excluding default ones."""
982 if self.cc is None:
983 self.cc = ','.join(self.watchers)
984 return self.cc
985
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000986 def SetWatchers(self, watchers):
987 """Set the list of email addresses that should be cc'd based on the changed
988 files in this CL.
989 """
990 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991
992 def GetBranch(self):
993 """Returns the short branch name, e.g. 'master'."""
994 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000995 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000996 if not branchref:
997 return None
998 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 self.branch = ShortBranchName(self.branchref)
1000 return self.branch
1001
1002 def GetBranchRef(self):
1003 """Returns the full branch name, e.g. 'refs/heads/master'."""
1004 self.GetBranch() # Poke the lazy loader.
1005 return self.branchref
1006
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001007 def ClearBranch(self):
1008 """Clears cached branch data of this object."""
1009 self.branch = self.branchref = None
1010
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001011 @staticmethod
1012 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001013 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 e.g. 'origin', 'refs/heads/master'
1015 """
1016 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1018 error_ok=True).strip()
1019 if upstream_branch:
1020 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1021 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001022 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1023 error_ok=True).strip()
1024 if upstream_branch:
1025 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001027 # Fall back on trying a git-svn upstream branch.
1028 if settings.GetIsGitSvn():
1029 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001031 # Else, try to guess the origin remote.
1032 remote_branches = RunGit(['branch', '-r']).split()
1033 if 'origin/master' in remote_branches:
1034 # Fall back on origin/master if it exits.
1035 remote = 'origin'
1036 upstream_branch = 'refs/heads/master'
1037 elif 'origin/trunk' in remote_branches:
1038 # Fall back on origin/trunk if it exists. Generally a shared
1039 # git-svn clone
1040 remote = 'origin'
1041 upstream_branch = 'refs/heads/trunk'
1042 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001043 DieWithError(
1044 'Unable to determine default branch to diff against.\n'
1045 'Either pass complete "git diff"-style arguments, like\n'
1046 ' git cl upload origin/master\n'
1047 'or verify this branch is set up to track another \n'
1048 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049
1050 return remote, upstream_branch
1051
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001052 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001053 upstream_branch = self.GetUpstreamBranch()
1054 if not BranchExists(upstream_branch):
1055 DieWithError('The upstream for the current branch (%s) does not exist '
1056 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001057 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001058 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001060 def GetUpstreamBranch(self):
1061 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001062 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001064 upstream_branch = upstream_branch.replace('refs/heads/',
1065 'refs/remotes/%s/' % remote)
1066 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1067 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068 self.upstream_branch = upstream_branch
1069 return self.upstream_branch
1070
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001071 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001072 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001073 remote, branch = None, self.GetBranch()
1074 seen_branches = set()
1075 while branch not in seen_branches:
1076 seen_branches.add(branch)
1077 remote, branch = self.FetchUpstreamTuple(branch)
1078 branch = ShortBranchName(branch)
1079 if remote != '.' or branch.startswith('refs/remotes'):
1080 break
1081 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001082 remotes = RunGit(['remote'], error_ok=True).split()
1083 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001084 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001085 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001087 logging.warning('Could not determine which remote this change is '
1088 'associated with, so defaulting to "%s". This may '
1089 'not be what you want. You may prevent this message '
1090 'by running "git svn info" as documented here: %s',
1091 self._remote,
1092 GIT_INSTRUCTIONS_URL)
1093 else:
1094 logging.warn('Could not determine which remote this change is '
1095 'associated with. You may prevent this message by '
1096 'running "git svn info" as documented here: %s',
1097 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 branch = 'HEAD'
1099 if branch.startswith('refs/remotes'):
1100 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001101 elif branch.startswith('refs/branch-heads/'):
1102 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 else:
1104 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001105 return self._remote
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 def GitSanityChecks(self, upstream_git_obj):
1108 """Checks git repo status and ensures diff is from local commits."""
1109
sbc@chromium.org79706062015-01-14 21:18:12 +00001110 if upstream_git_obj is None:
1111 if self.GetBranch() is None:
1112 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001113 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001114 else:
1115 print >> sys.stderr, (
1116 'ERROR: no upstream branch')
1117 return False
1118
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001119 # Verify the commit we're diffing against is in our current branch.
1120 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1121 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1122 if upstream_sha != common_ancestor:
1123 print >> sys.stderr, (
1124 'ERROR: %s is not in the current branch. You may need to rebase '
1125 'your tracking branch' % upstream_sha)
1126 return False
1127
1128 # List the commits inside the diff, and verify they are all local.
1129 commits_in_diff = RunGit(
1130 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1131 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1132 remote_branch = remote_branch.strip()
1133 if code != 0:
1134 _, remote_branch = self.GetRemoteBranch()
1135
1136 commits_in_remote = RunGit(
1137 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1138
1139 common_commits = set(commits_in_diff) & set(commits_in_remote)
1140 if common_commits:
1141 print >> sys.stderr, (
1142 'ERROR: Your diff contains %d commits already in %s.\n'
1143 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1144 'the diff. If you are using a custom git flow, you can override'
1145 ' the reference used for this check with "git config '
1146 'gitcl.remotebranch <git-ref>".' % (
1147 len(common_commits), remote_branch, upstream_git_obj))
1148 return False
1149 return True
1150
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001151 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001152 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001153
1154 Returns None if it is not set.
1155 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001156 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1157 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001158
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001159 def GetGitSvnRemoteUrl(self):
1160 """Return the configured git-svn remote URL parsed from git svn info.
1161
1162 Returns None if it is not set.
1163 """
1164 # URL is dependent on the current directory.
1165 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1166 if data:
1167 keys = dict(line.split(': ', 1) for line in data.splitlines()
1168 if ': ' in line)
1169 return keys.get('URL', None)
1170 return None
1171
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 def GetRemoteUrl(self):
1173 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1174
1175 Returns None if there is no remote.
1176 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001177 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001178 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1179
1180 # If URL is pointing to a local directory, it is probably a git cache.
1181 if os.path.isdir(url):
1182 url = RunGit(['config', 'remote.%s.url' % remote],
1183 error_ok=True,
1184 cwd=url).strip()
1185 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001187 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001188 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001189 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 issue = RunGit(['config',
1191 self._codereview_impl.IssueSetting(self.GetBranch())],
1192 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001193 self.issue = int(issue) or None if issue else None
1194 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 return self.issue
1196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197 def GetIssueURL(self):
1198 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001199 issue = self.GetIssue()
1200 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001201 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203
1204 def GetDescription(self, pretty=False):
1205 if not self.has_description:
1206 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001207 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208 self.has_description = True
1209 if pretty:
1210 wrapper = textwrap.TextWrapper()
1211 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1212 return wrapper.fill(self.description)
1213 return self.description
1214
1215 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001216 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001218 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001220 self.patchset = int(patchset) or None if patchset else None
1221 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 return self.patchset
1223
1224 def SetPatchset(self, patchset):
1225 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001226 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001231 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001232 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001235 def SetIssue(self, issue=None):
1236 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001237 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1238 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001240 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 RunGit(['config', issue_setting, str(issue)])
1242 codereview_server = self._codereview_impl.GetCodereviewServer()
1243 if codereview_server:
1244 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001246 current_issue = self.GetIssue()
1247 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001248 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001249 self.issue = None
1250 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001252 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001253 if not self.GitSanityChecks(upstream_branch):
1254 DieWithError('\nGit sanity check failure')
1255
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001256 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001257 if not root:
1258 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001259 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001260
1261 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001262 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001263 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001264 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001265 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001266 except subprocess2.CalledProcessError:
1267 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001268 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001269 'This branch probably doesn\'t exist anymore. To reset the\n'
1270 'tracking branch, please run\n'
1271 ' git branch --set-upstream %s trunk\n'
1272 'replacing trunk with origin/master or the relevant branch') %
1273 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001274
maruel@chromium.org52424302012-08-29 15:14:30 +00001275 issue = self.GetIssue()
1276 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001277 if issue:
1278 description = self.GetDescription()
1279 else:
1280 # If the change was never uploaded, use the log messages of all commits
1281 # up to the branch point, as git cl upload will prefill the description
1282 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001283 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1284 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001285
1286 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001287 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001288 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001289 name,
1290 description,
1291 absroot,
1292 files,
1293 issue,
1294 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001295 author,
1296 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001297
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001298 def UpdateDescription(self, description):
1299 self.description = description
1300 return self._codereview_impl.UpdateDescriptionRemote(description)
1301
1302 def RunHook(self, committing, may_prompt, verbose, change):
1303 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1304 try:
1305 return presubmit_support.DoPresubmitChecks(change, committing,
1306 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1307 default_presubmit=None, may_prompt=may_prompt,
1308 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1309 except presubmit_support.PresubmitFailure, e:
1310 DieWithError(
1311 ('%s\nMaybe your depot_tools is out of date?\n'
1312 'If all fails, contact maruel@') % e)
1313
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001314 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1315 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001316 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1317 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001318 else:
1319 # Assume url.
1320 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1321 urlparse.urlparse(issue_arg))
1322 if not parsed_issue_arg or not parsed_issue_arg.valid:
1323 DieWithError('Failed to parse issue argument "%s". '
1324 'Must be an issue number or a valid URL.' % issue_arg)
1325 return self._codereview_impl.CMDPatchWithParsedIssue(
1326 parsed_issue_arg, reject, nocommit, directory)
1327
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001328 def CMDUpload(self, options, git_diff_args, orig_args):
1329 """Uploads a change to codereview."""
1330 if git_diff_args:
1331 # TODO(ukai): is it ok for gerrit case?
1332 base_branch = git_diff_args[0]
1333 else:
1334 if self.GetBranch() is None:
1335 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1336
1337 # Default to diffing against common ancestor of upstream branch
1338 base_branch = self.GetCommonAncestorWithUpstream()
1339 git_diff_args = [base_branch, 'HEAD']
1340
1341 # Make sure authenticated to codereview before running potentially expensive
1342 # hooks. It is a fast, best efforts check. Codereview still can reject the
1343 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001344 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001345
1346 # Apply watchlists on upload.
1347 change = self.GetChange(base_branch, None)
1348 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1349 files = [f.LocalPath() for f in change.AffectedFiles()]
1350 if not options.bypass_watchlists:
1351 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1352
1353 if not options.bypass_hooks:
1354 if options.reviewers or options.tbr_owners:
1355 # Set the reviewer list now so that presubmit checks can access it.
1356 change_description = ChangeDescription(change.FullDescriptionText())
1357 change_description.update_reviewers(options.reviewers,
1358 options.tbr_owners,
1359 change)
1360 change.SetDescriptionText(change_description.description)
1361 hook_results = self.RunHook(committing=False,
1362 may_prompt=not options.force,
1363 verbose=options.verbose,
1364 change=change)
1365 if not hook_results.should_continue():
1366 return 1
1367 if not options.reviewers and hook_results.reviewers:
1368 options.reviewers = hook_results.reviewers.split(',')
1369
1370 if self.GetIssue():
1371 latest_patchset = self.GetMostRecentPatchset()
1372 local_patchset = self.GetPatchset()
1373 if (latest_patchset and local_patchset and
1374 local_patchset != latest_patchset):
1375 print ('The last upload made from this repository was patchset #%d but '
1376 'the most recent patchset on the server is #%d.'
1377 % (local_patchset, latest_patchset))
1378 print ('Uploading will still work, but if you\'ve uploaded to this '
1379 'issue from another machine or branch the patch you\'re '
1380 'uploading now might not include those changes.')
1381 ask_for_data('About to upload; enter to confirm.')
1382
1383 print_stats(options.similarity, options.find_copies, git_diff_args)
1384 ret = self.CMDUploadChange(options, git_diff_args, change)
1385 if not ret:
1386 git_set_branch_value('last-upload-hash',
1387 RunGit(['rev-parse', 'HEAD']).strip())
1388 # Run post upload hooks, if specified.
1389 if settings.GetRunPostUploadHook():
1390 presubmit_support.DoPostUploadExecuter(
1391 change,
1392 self,
1393 settings.GetRoot(),
1394 options.verbose,
1395 sys.stdout)
1396
1397 # Upload all dependencies if specified.
1398 if options.dependencies:
1399 print
1400 print '--dependencies has been specified.'
1401 print 'All dependent local branches will be re-uploaded.'
1402 print
1403 # Remove the dependencies flag from args so that we do not end up in a
1404 # loop.
1405 orig_args.remove('--dependencies')
1406 ret = upload_branch_deps(self, orig_args)
1407 return ret
1408
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001409 def SetCQState(self, new_state):
1410 """Update the CQ state for latest patchset.
1411
1412 Issue must have been already uploaded and known.
1413 """
1414 assert new_state in _CQState.ALL_STATES
1415 assert self.GetIssue()
1416 return self._codereview_impl.SetCQState(new_state)
1417
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001418 # Forward methods to codereview specific implementation.
1419
1420 def CloseIssue(self):
1421 return self._codereview_impl.CloseIssue()
1422
1423 def GetStatus(self):
1424 return self._codereview_impl.GetStatus()
1425
1426 def GetCodereviewServer(self):
1427 return self._codereview_impl.GetCodereviewServer()
1428
1429 def GetApprovingReviewers(self):
1430 return self._codereview_impl.GetApprovingReviewers()
1431
1432 def GetMostRecentPatchset(self):
1433 return self._codereview_impl.GetMostRecentPatchset()
1434
1435 def __getattr__(self, attr):
1436 # This is because lots of untested code accesses Rietveld-specific stuff
1437 # directly, and it's hard to fix for sure. So, just let it work, and fix
1438 # on a cases by case basis.
1439 return getattr(self._codereview_impl, attr)
1440
1441
1442class _ChangelistCodereviewBase(object):
1443 """Abstract base class encapsulating codereview specifics of a changelist."""
1444 def __init__(self, changelist):
1445 self._changelist = changelist # instance of Changelist
1446
1447 def __getattr__(self, attr):
1448 # Forward methods to changelist.
1449 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1450 # _RietveldChangelistImpl to avoid this hack?
1451 return getattr(self._changelist, attr)
1452
1453 def GetStatus(self):
1454 """Apply a rough heuristic to give a simple summary of an issue's review
1455 or CQ status, assuming adherence to a common workflow.
1456
1457 Returns None if no issue for this branch, or specific string keywords.
1458 """
1459 raise NotImplementedError()
1460
1461 def GetCodereviewServer(self):
1462 """Returns server URL without end slash, like "https://codereview.com"."""
1463 raise NotImplementedError()
1464
1465 def FetchDescription(self):
1466 """Fetches and returns description from the codereview server."""
1467 raise NotImplementedError()
1468
1469 def GetCodereviewServerSetting(self):
1470 """Returns git config setting for the codereview server."""
1471 raise NotImplementedError()
1472
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001473 @classmethod
1474 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001475 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001476
1477 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001478 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001479 """Returns name of git config setting which stores issue number for a given
1480 branch."""
1481 raise NotImplementedError()
1482
1483 def PatchsetSetting(self):
1484 """Returns name of git config setting which stores issue number."""
1485 raise NotImplementedError()
1486
1487 def GetRieveldObjForPresubmit(self):
1488 # This is an unfortunate Rietveld-embeddedness in presubmit.
1489 # For non-Rietveld codereviews, this probably should return a dummy object.
1490 raise NotImplementedError()
1491
1492 def UpdateDescriptionRemote(self, description):
1493 """Update the description on codereview site."""
1494 raise NotImplementedError()
1495
1496 def CloseIssue(self):
1497 """Closes the issue."""
1498 raise NotImplementedError()
1499
1500 def GetApprovingReviewers(self):
1501 """Returns a list of reviewers approving the change.
1502
1503 Note: not necessarily committers.
1504 """
1505 raise NotImplementedError()
1506
1507 def GetMostRecentPatchset(self):
1508 """Returns the most recent patchset number from the codereview site."""
1509 raise NotImplementedError()
1510
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001511 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1512 directory):
1513 """Fetches and applies the issue.
1514
1515 Arguments:
1516 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1517 reject: if True, reject the failed patch instead of switching to 3-way
1518 merge. Rietveld only.
1519 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1520 only.
1521 directory: switch to directory before applying the patch. Rietveld only.
1522 """
1523 raise NotImplementedError()
1524
1525 @staticmethod
1526 def ParseIssueURL(parsed_url):
1527 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1528 failed."""
1529 raise NotImplementedError()
1530
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001531 def EnsureAuthenticated(self, force):
1532 """Best effort check that user is authenticated with codereview server.
1533
1534 Arguments:
1535 force: whether to skip confirmation questions.
1536 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001537 raise NotImplementedError()
1538
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001539 def CMDUploadChange(self, options, args, change):
1540 """Uploads a change to codereview."""
1541 raise NotImplementedError()
1542
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001543 def SetCQState(self, new_state):
1544 """Update the CQ state for latest patchset.
1545
1546 Issue must have been already uploaded and known.
1547 """
1548 raise NotImplementedError()
1549
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001550
1551class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1552 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1553 super(_RietveldChangelistImpl, self).__init__(changelist)
1554 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1555 settings.GetDefaultServerUrl()
1556
1557 self._rietveld_server = rietveld_server
1558 self._auth_config = auth_config
1559 self._props = None
1560 self._rpc_server = None
1561
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001562 def GetCodereviewServer(self):
1563 if not self._rietveld_server:
1564 # If we're on a branch then get the server potentially associated
1565 # with that branch.
1566 if self.GetIssue():
1567 rietveld_server_setting = self.GetCodereviewServerSetting()
1568 if rietveld_server_setting:
1569 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1570 ['config', rietveld_server_setting], error_ok=True).strip())
1571 if not self._rietveld_server:
1572 self._rietveld_server = settings.GetDefaultServerUrl()
1573 return self._rietveld_server
1574
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001575 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001576 """Best effort check that user is authenticated with Rietveld server."""
1577 if self._auth_config.use_oauth2:
1578 authenticator = auth.get_authenticator_for_host(
1579 self.GetCodereviewServer(), self._auth_config)
1580 if not authenticator.has_cached_credentials():
1581 raise auth.LoginRequiredError(self.GetCodereviewServer())
1582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583 def FetchDescription(self):
1584 issue = self.GetIssue()
1585 assert issue
1586 try:
1587 return self.RpcServer().get_description(issue).strip()
1588 except urllib2.HTTPError as e:
1589 if e.code == 404:
1590 DieWithError(
1591 ('\nWhile fetching the description for issue %d, received a '
1592 '404 (not found)\n'
1593 'error. It is likely that you deleted this '
1594 'issue on the server. If this is the\n'
1595 'case, please run\n\n'
1596 ' git cl issue 0\n\n'
1597 'to clear the association with the deleted issue. Then run '
1598 'this command again.') % issue)
1599 else:
1600 DieWithError(
1601 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1602 except urllib2.URLError as e:
1603 print >> sys.stderr, (
1604 'Warning: Failed to retrieve CL description due to network '
1605 'failure.')
1606 return ''
1607
1608 def GetMostRecentPatchset(self):
1609 return self.GetIssueProperties()['patchsets'][-1]
1610
1611 def GetPatchSetDiff(self, issue, patchset):
1612 return self.RpcServer().get(
1613 '/download/issue%s_%s.diff' % (issue, patchset))
1614
1615 def GetIssueProperties(self):
1616 if self._props is None:
1617 issue = self.GetIssue()
1618 if not issue:
1619 self._props = {}
1620 else:
1621 self._props = self.RpcServer().get_issue_properties(issue, True)
1622 return self._props
1623
1624 def GetApprovingReviewers(self):
1625 return get_approving_reviewers(self.GetIssueProperties())
1626
1627 def AddComment(self, message):
1628 return self.RpcServer().add_comment(self.GetIssue(), message)
1629
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001630 def GetStatus(self):
1631 """Apply a rough heuristic to give a simple summary of an issue's review
1632 or CQ status, assuming adherence to a common workflow.
1633
1634 Returns None if no issue for this branch, or one of the following keywords:
1635 * 'error' - error from review tool (including deleted issues)
1636 * 'unsent' - not sent for review
1637 * 'waiting' - waiting for review
1638 * 'reply' - waiting for owner to reply to review
1639 * 'lgtm' - LGTM from at least one approved reviewer
1640 * 'commit' - in the commit queue
1641 * 'closed' - closed
1642 """
1643 if not self.GetIssue():
1644 return None
1645
1646 try:
1647 props = self.GetIssueProperties()
1648 except urllib2.HTTPError:
1649 return 'error'
1650
1651 if props.get('closed'):
1652 # Issue is closed.
1653 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001654 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001655 # Issue is in the commit queue.
1656 return 'commit'
1657
1658 try:
1659 reviewers = self.GetApprovingReviewers()
1660 except urllib2.HTTPError:
1661 return 'error'
1662
1663 if reviewers:
1664 # Was LGTM'ed.
1665 return 'lgtm'
1666
1667 messages = props.get('messages') or []
1668
1669 if not messages:
1670 # No message was sent.
1671 return 'unsent'
1672 if messages[-1]['sender'] != props.get('owner_email'):
1673 # Non-LGTM reply from non-owner
1674 return 'reply'
1675 return 'waiting'
1676
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001677 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001678 return self.RpcServer().update_description(
1679 self.GetIssue(), self.description)
1680
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001682 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001683
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001684 def SetFlag(self, flag, value):
1685 """Patchset must match."""
1686 if not self.GetPatchset():
1687 DieWithError('The patchset needs to match. Send another patchset.')
1688 try:
1689 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001690 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001691 except urllib2.HTTPError, e:
1692 if e.code == 404:
1693 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1694 if e.code == 403:
1695 DieWithError(
1696 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1697 'match?') % (self.GetIssue(), self.GetPatchset()))
1698 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001699
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001700 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701 """Returns an upload.RpcServer() to access this review's rietveld instance.
1702 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001703 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001704 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001705 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001706 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001707 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001709 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001710 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001711 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001712
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001713 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001714 """Return the git setting that stores this change's most recent patchset."""
1715 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1716
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001717 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001719 branch = self.GetBranch()
1720 if branch:
1721 return 'branch.%s.rietveldserver' % branch
1722 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001724 def GetRieveldObjForPresubmit(self):
1725 return self.RpcServer()
1726
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001727 def SetCQState(self, new_state):
1728 props = self.GetIssueProperties()
1729 if props.get('private'):
1730 DieWithError('Cannot set-commit on private issue')
1731
1732 if new_state == _CQState.COMMIT:
1733 self.SetFlag('commit', '1')
1734 elif new_state == _CQState.NONE:
1735 self.SetFlag('commit', '0')
1736 else:
1737 raise NotImplementedError()
1738
1739
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001740 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1741 directory):
1742 # TODO(maruel): Use apply_issue.py
1743
1744 # PatchIssue should never be called with a dirty tree. It is up to the
1745 # caller to check this, but just in case we assert here since the
1746 # consequences of the caller not checking this could be dire.
1747 assert(not git_common.is_dirty_git_tree('apply'))
1748 assert(parsed_issue_arg.valid)
1749 self._changelist.issue = parsed_issue_arg.issue
1750 if parsed_issue_arg.hostname:
1751 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1752
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001753 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1754 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001755 assert parsed_issue_arg.patchset
1756 patchset = parsed_issue_arg.patchset
1757 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1758 else:
1759 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1760 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1761
1762 # Switch up to the top-level directory, if necessary, in preparation for
1763 # applying the patch.
1764 top = settings.GetRelativeRoot()
1765 if top:
1766 os.chdir(top)
1767
1768 # Git patches have a/ at the beginning of source paths. We strip that out
1769 # with a sed script rather than the -p flag to patch so we can feed either
1770 # Git or svn-style patches into the same apply command.
1771 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1772 try:
1773 patch_data = subprocess2.check_output(
1774 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1775 except subprocess2.CalledProcessError:
1776 DieWithError('Git patch mungling failed.')
1777 logging.info(patch_data)
1778
1779 # We use "git apply" to apply the patch instead of "patch" so that we can
1780 # pick up file adds.
1781 # The --index flag means: also insert into the index (so we catch adds).
1782 cmd = ['git', 'apply', '--index', '-p0']
1783 if directory:
1784 cmd.extend(('--directory', directory))
1785 if reject:
1786 cmd.append('--reject')
1787 elif IsGitVersionAtLeast('1.7.12'):
1788 cmd.append('--3way')
1789 try:
1790 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1791 stdin=patch_data, stdout=subprocess2.VOID)
1792 except subprocess2.CalledProcessError:
1793 print 'Failed to apply the patch'
1794 return 1
1795
1796 # If we had an issue, commit the current state and register the issue.
1797 if not nocommit:
1798 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1799 'patch from issue %(i)s at patchset '
1800 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1801 % {'i': self.GetIssue(), 'p': patchset})])
1802 self.SetIssue(self.GetIssue())
1803 self.SetPatchset(patchset)
1804 print "Committed patch locally."
1805 else:
1806 print "Patch applied to index."
1807 return 0
1808
1809 @staticmethod
1810 def ParseIssueURL(parsed_url):
1811 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1812 return None
1813 # Typical url: https://domain/<issue_number>[/[other]]
1814 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1815 if match:
1816 return _RietveldParsedIssueNumberArgument(
1817 issue=int(match.group(1)),
1818 hostname=parsed_url.netloc)
1819 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1820 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1821 if match:
1822 return _RietveldParsedIssueNumberArgument(
1823 issue=int(match.group(1)),
1824 patchset=int(match.group(2)),
1825 hostname=parsed_url.netloc,
1826 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1827 return None
1828
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001829 def CMDUploadChange(self, options, args, change):
1830 """Upload the patch to Rietveld."""
1831 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1832 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001833 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1834 if options.emulate_svn_auto_props:
1835 upload_args.append('--emulate_svn_auto_props')
1836
1837 change_desc = None
1838
1839 if options.email is not None:
1840 upload_args.extend(['--email', options.email])
1841
1842 if self.GetIssue():
1843 if options.title:
1844 upload_args.extend(['--title', options.title])
1845 if options.message:
1846 upload_args.extend(['--message', options.message])
1847 upload_args.extend(['--issue', str(self.GetIssue())])
1848 print ('This branch is associated with issue %s. '
1849 'Adding patch to that issue.' % self.GetIssue())
1850 else:
1851 if options.title:
1852 upload_args.extend(['--title', options.title])
1853 message = (options.title or options.message or
1854 CreateDescriptionFromLog(args))
1855 change_desc = ChangeDescription(message)
1856 if options.reviewers or options.tbr_owners:
1857 change_desc.update_reviewers(options.reviewers,
1858 options.tbr_owners,
1859 change)
1860 if not options.force:
1861 change_desc.prompt()
1862
1863 if not change_desc.description:
1864 print "Description is empty; aborting."
1865 return 1
1866
1867 upload_args.extend(['--message', change_desc.description])
1868 if change_desc.get_reviewers():
1869 upload_args.append('--reviewers=%s' % ','.join(
1870 change_desc.get_reviewers()))
1871 if options.send_mail:
1872 if not change_desc.get_reviewers():
1873 DieWithError("Must specify reviewers to send email.")
1874 upload_args.append('--send_mail')
1875
1876 # We check this before applying rietveld.private assuming that in
1877 # rietveld.cc only addresses which we can send private CLs to are listed
1878 # if rietveld.private is set, and so we should ignore rietveld.cc only
1879 # when --private is specified explicitly on the command line.
1880 if options.private:
1881 logging.warn('rietveld.cc is ignored since private flag is specified. '
1882 'You need to review and add them manually if necessary.')
1883 cc = self.GetCCListWithoutDefault()
1884 else:
1885 cc = self.GetCCList()
1886 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1887 if cc:
1888 upload_args.extend(['--cc', cc])
1889
1890 if options.private or settings.GetDefaultPrivateFlag() == "True":
1891 upload_args.append('--private')
1892
1893 upload_args.extend(['--git_similarity', str(options.similarity)])
1894 if not options.find_copies:
1895 upload_args.extend(['--git_no_find_copies'])
1896
1897 # Include the upstream repo's URL in the change -- this is useful for
1898 # projects that have their source spread across multiple repos.
1899 remote_url = self.GetGitBaseUrlFromConfig()
1900 if not remote_url:
1901 if settings.GetIsGitSvn():
1902 remote_url = self.GetGitSvnRemoteUrl()
1903 else:
1904 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1905 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1906 self.GetUpstreamBranch().split('/')[-1])
1907 if remote_url:
1908 upload_args.extend(['--base_url', remote_url])
1909 remote, remote_branch = self.GetRemoteBranch()
1910 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1911 settings.GetPendingRefPrefix())
1912 if target_ref:
1913 upload_args.extend(['--target_ref', target_ref])
1914
1915 # Look for dependent patchsets. See crbug.com/480453 for more details.
1916 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1917 upstream_branch = ShortBranchName(upstream_branch)
1918 if remote is '.':
1919 # A local branch is being tracked.
1920 local_branch = ShortBranchName(upstream_branch)
1921 if settings.GetIsSkipDependencyUpload(local_branch):
1922 print
1923 print ('Skipping dependency patchset upload because git config '
1924 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1925 print
1926 else:
1927 auth_config = auth.extract_auth_config_from_options(options)
1928 branch_cl = Changelist(branchref=local_branch,
1929 auth_config=auth_config)
1930 branch_cl_issue_url = branch_cl.GetIssueURL()
1931 branch_cl_issue = branch_cl.GetIssue()
1932 branch_cl_patchset = branch_cl.GetPatchset()
1933 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1934 upload_args.extend(
1935 ['--depends_on_patchset', '%s:%s' % (
1936 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001937 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001938 '\n'
1939 'The current branch (%s) is tracking a local branch (%s) with '
1940 'an associated CL.\n'
1941 'Adding %s/#ps%s as a dependency patchset.\n'
1942 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1943 branch_cl_patchset))
1944
1945 project = settings.GetProject()
1946 if project:
1947 upload_args.extend(['--project', project])
1948
1949 if options.cq_dry_run:
1950 upload_args.extend(['--cq_dry_run'])
1951
1952 try:
1953 upload_args = ['upload'] + upload_args + args
1954 logging.info('upload.RealMain(%s)', upload_args)
1955 issue, patchset = upload.RealMain(upload_args)
1956 issue = int(issue)
1957 patchset = int(patchset)
1958 except KeyboardInterrupt:
1959 sys.exit(1)
1960 except:
1961 # If we got an exception after the user typed a description for their
1962 # change, back up the description before re-raising.
1963 if change_desc:
1964 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1965 print('\nGot exception while uploading -- saving description to %s\n' %
1966 backup_path)
1967 backup_file = open(backup_path, 'w')
1968 backup_file.write(change_desc.description)
1969 backup_file.close()
1970 raise
1971
1972 if not self.GetIssue():
1973 self.SetIssue(issue)
1974 self.SetPatchset(patchset)
1975
1976 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001977 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001978 return 0
1979
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001980
1981class _GerritChangelistImpl(_ChangelistCodereviewBase):
1982 def __init__(self, changelist, auth_config=None):
1983 # auth_config is Rietveld thing, kept here to preserve interface only.
1984 super(_GerritChangelistImpl, self).__init__(changelist)
1985 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001986 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001987 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001988 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001989
1990 def _GetGerritHost(self):
1991 # Lazy load of configs.
1992 self.GetCodereviewServer()
1993 return self._gerrit_host
1994
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001995 def _GetGitHost(self):
1996 """Returns git host to be used when uploading change to Gerrit."""
1997 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1998
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001999 def GetCodereviewServer(self):
2000 if not self._gerrit_server:
2001 # If we're on a branch then get the server potentially associated
2002 # with that branch.
2003 if self.GetIssue():
2004 gerrit_server_setting = self.GetCodereviewServerSetting()
2005 if gerrit_server_setting:
2006 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2007 error_ok=True).strip()
2008 if self._gerrit_server:
2009 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2010 if not self._gerrit_server:
2011 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2012 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002013 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002014 parts[0] = parts[0] + '-review'
2015 self._gerrit_host = '.'.join(parts)
2016 self._gerrit_server = 'https://%s' % self._gerrit_host
2017 return self._gerrit_server
2018
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002019 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002020 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002021 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002022
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002024 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002025 # Lazy-loader to identify Gerrit and Git hosts.
2026 if gerrit_util.GceAuthenticator.is_gce():
2027 return
2028 self.GetCodereviewServer()
2029 git_host = self._GetGitHost()
2030 assert self._gerrit_server and self._gerrit_host
2031 cookie_auth = gerrit_util.CookiesAuthenticator()
2032
2033 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2034 git_auth = cookie_auth.get_auth_header(git_host)
2035 if gerrit_auth and git_auth:
2036 if gerrit_auth == git_auth:
2037 return
2038 print((
2039 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2040 ' Check your %s or %s file for credentials of hosts:\n'
2041 ' %s\n'
2042 ' %s\n'
2043 ' %s') %
2044 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2045 git_host, self._gerrit_host,
2046 cookie_auth.get_new_password_message(git_host)))
2047 if not force:
2048 ask_for_data('If you know what you are doing, press Enter to continue, '
2049 'Ctrl+C to abort.')
2050 return
2051 else:
2052 missing = (
2053 [] if gerrit_auth else [self._gerrit_host] +
2054 [] if git_auth else [git_host])
2055 DieWithError('Credentials for the following hosts are required:\n'
2056 ' %s\n'
2057 'These are read from %s (or legacy %s)\n'
2058 '%s' % (
2059 '\n '.join(missing),
2060 cookie_auth.get_gitcookies_path(),
2061 cookie_auth.get_netrc_path(),
2062 cookie_auth.get_new_password_message(git_host)))
2063
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002064
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002065 def PatchsetSetting(self):
2066 """Return the git setting that stores this change's most recent patchset."""
2067 return 'branch.%s.gerritpatchset' % self.GetBranch()
2068
2069 def GetCodereviewServerSetting(self):
2070 """Returns the git setting that stores this change's Gerrit server."""
2071 branch = self.GetBranch()
2072 if branch:
2073 return 'branch.%s.gerritserver' % branch
2074 return None
2075
2076 def GetRieveldObjForPresubmit(self):
2077 class ThisIsNotRietveldIssue(object):
2078 def __nonzero__(self):
2079 # This is a hack to make presubmit_support think that rietveld is not
2080 # defined, yet still ensure that calls directly result in a decent
2081 # exception message below.
2082 return False
2083
2084 def __getattr__(self, attr):
2085 print(
2086 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2087 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2088 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2089 'or use Rietveld for codereview.\n'
2090 'See also http://crbug.com/579160.' % attr)
2091 raise NotImplementedError()
2092 return ThisIsNotRietveldIssue()
2093
2094 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002095 """Apply a rough heuristic to give a simple summary of an issue's review
2096 or CQ status, assuming adherence to a common workflow.
2097
2098 Returns None if no issue for this branch, or one of the following keywords:
2099 * 'error' - error from review tool (including deleted issues)
2100 * 'unsent' - no reviewers added
2101 * 'waiting' - waiting for review
2102 * 'reply' - waiting for owner to reply to review
2103 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2104 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2105 * 'commit' - in the commit queue
2106 * 'closed' - abandoned
2107 """
2108 if not self.GetIssue():
2109 return None
2110
2111 try:
2112 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2113 except httplib.HTTPException:
2114 return 'error'
2115
2116 if data['status'] == 'ABANDONED':
2117 return 'closed'
2118
2119 cq_label = data['labels'].get('Commit-Queue', {})
2120 if cq_label:
2121 # Vote value is a stringified integer, which we expect from 0 to 2.
2122 vote_value = cq_label.get('value', '0')
2123 vote_text = cq_label.get('values', {}).get(vote_value, '')
2124 if vote_text.lower() == 'commit':
2125 return 'commit'
2126
2127 lgtm_label = data['labels'].get('Code-Review', {})
2128 if lgtm_label:
2129 if 'rejected' in lgtm_label:
2130 return 'not lgtm'
2131 if 'approved' in lgtm_label:
2132 return 'lgtm'
2133
2134 if not data.get('reviewers', {}).get('REVIEWER', []):
2135 return 'unsent'
2136
2137 messages = data.get('messages', [])
2138 if messages:
2139 owner = data['owner'].get('_account_id')
2140 last_message_author = messages[-1].get('author', {}).get('_account_id')
2141 if owner != last_message_author:
2142 # Some reply from non-owner.
2143 return 'reply'
2144
2145 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002146
2147 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002148 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002149 return data['revisions'][data['current_revision']]['_number']
2150
2151 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002152 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002153 return data['revisions'][data['current_revision']]['commit_with_footers']
2154
2155 def UpdateDescriptionRemote(self, description):
2156 # TODO(tandrii)
2157 raise NotImplementedError()
2158
2159 def CloseIssue(self):
2160 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2161
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002162 def SubmitIssue(self, wait_for_merge=True):
2163 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2164 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002166 def _GetChangeDetail(self, options=None, issue=None):
2167 options = options or []
2168 issue = issue or self.GetIssue()
2169 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002170 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2171 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002172
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002173 def CMDLand(self, force, bypass_hooks, verbose):
2174 if git_common.is_dirty_git_tree('land'):
2175 return 1
2176 differs = True
2177 last_upload = RunGit(['config',
2178 'branch.%s.gerritsquashhash' % self.GetBranch()],
2179 error_ok=True).strip()
2180 # Note: git diff outputs nothing if there is no diff.
2181 if not last_upload or RunGit(['diff', last_upload]).strip():
2182 print('WARNING: some changes from local branch haven\'t been uploaded')
2183 else:
2184 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2185 if detail['current_revision'] == last_upload:
2186 differs = False
2187 else:
2188 print('WARNING: local branch contents differ from latest uploaded '
2189 'patchset')
2190 if differs:
2191 if not force:
2192 ask_for_data(
2193 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2194 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2195 elif not bypass_hooks:
2196 hook_results = self.RunHook(
2197 committing=True,
2198 may_prompt=not force,
2199 verbose=verbose,
2200 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2201 if not hook_results.should_continue():
2202 return 1
2203
2204 self.SubmitIssue(wait_for_merge=True)
2205 print('Issue %s has been submitted.' % self.GetIssueURL())
2206 return 0
2207
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002208 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2209 directory):
2210 assert not reject
2211 assert not nocommit
2212 assert not directory
2213 assert parsed_issue_arg.valid
2214
2215 self._changelist.issue = parsed_issue_arg.issue
2216
2217 if parsed_issue_arg.hostname:
2218 self._gerrit_host = parsed_issue_arg.hostname
2219 self._gerrit_server = 'https://%s' % self._gerrit_host
2220
2221 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2222
2223 if not parsed_issue_arg.patchset:
2224 # Use current revision by default.
2225 revision_info = detail['revisions'][detail['current_revision']]
2226 patchset = int(revision_info['_number'])
2227 else:
2228 patchset = parsed_issue_arg.patchset
2229 for revision_info in detail['revisions'].itervalues():
2230 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2231 break
2232 else:
2233 DieWithError('Couldn\'t find patchset %i in issue %i' %
2234 (parsed_issue_arg.patchset, self.GetIssue()))
2235
2236 fetch_info = revision_info['fetch']['http']
2237 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2238 RunGit(['cherry-pick', 'FETCH_HEAD'])
2239 self.SetIssue(self.GetIssue())
2240 self.SetPatchset(patchset)
2241 print('Committed patch for issue %i pathset %i locally' %
2242 (self.GetIssue(), self.GetPatchset()))
2243 return 0
2244
2245 @staticmethod
2246 def ParseIssueURL(parsed_url):
2247 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2248 return None
2249 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2250 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2251 # Short urls like https://domain/<issue_number> can be used, but don't allow
2252 # specifying the patchset (you'd 404), but we allow that here.
2253 if parsed_url.path == '/':
2254 part = parsed_url.fragment
2255 else:
2256 part = parsed_url.path
2257 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2258 if match:
2259 return _ParsedIssueNumberArgument(
2260 issue=int(match.group(2)),
2261 patchset=int(match.group(4)) if match.group(4) else None,
2262 hostname=parsed_url.netloc)
2263 return None
2264
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002265 def CMDUploadChange(self, options, args, change):
2266 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002267 if options.squash and options.no_squash:
2268 DieWithError('Can only use one of --squash or --no-squash')
2269 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2270 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002271 # We assume the remote called "origin" is the one we want.
2272 # It is probably not worthwhile to support different workflows.
2273 gerrit_remote = 'origin'
2274
2275 remote, remote_branch = self.GetRemoteBranch()
2276 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2277 pending_prefix='')
2278
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002279 if options.squash:
2280 if not self.GetIssue():
2281 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2282 # with shadow branch, which used to contain change-id for a given
2283 # branch, using which we can fetch actual issue number and set it as the
2284 # property of the branch, which is the new way.
2285 message = RunGitSilent([
2286 'show', '--format=%B', '-s',
2287 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2288 if message:
2289 change_ids = git_footers.get_footer_change_id(message.strip())
2290 if change_ids and len(change_ids) == 1:
2291 details = self._GetChangeDetail(issue=change_ids[0])
2292 if details:
2293 print('WARNING: found old upload in branch git_cl_uploads/%s '
2294 'corresponding to issue %s' %
2295 (self.GetBranch(), details['_number']))
2296 self.SetIssue(details['_number'])
2297 if not self.GetIssue():
2298 DieWithError(
2299 '\n' # For readability of the blob below.
2300 'Found old upload in branch git_cl_uploads/%s, '
2301 'but failed to find corresponding Gerrit issue.\n'
2302 'If you know the issue number, set it manually first:\n'
2303 ' git cl issue 123456\n'
2304 'If you intended to upload this CL as new issue, '
2305 'just delete or rename the old upload branch:\n'
2306 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2307 'After that, please run git cl upload again.' %
2308 tuple([self.GetBranch()] * 3))
2309 # End of backwards compatability.
2310
2311 if self.GetIssue():
2312 # Try to get the message from a previous upload.
2313 message = self.GetDescription()
2314 if not message:
2315 DieWithError(
2316 'failed to fetch description from current Gerrit issue %d\n'
2317 '%s' % (self.GetIssue(), self.GetIssueURL()))
2318 change_id = self._GetChangeDetail()['change_id']
2319 while True:
2320 footer_change_ids = git_footers.get_footer_change_id(message)
2321 if footer_change_ids == [change_id]:
2322 break
2323 if not footer_change_ids:
2324 message = git_footers.add_footer_change_id(message, change_id)
2325 print('WARNING: appended missing Change-Id to issue description')
2326 continue
2327 # There is already a valid footer but with different or several ids.
2328 # Doing this automatically is non-trivial as we don't want to lose
2329 # existing other footers, yet we want to append just 1 desired
2330 # Change-Id. Thus, just create a new footer, but let user verify the
2331 # new description.
2332 message = '%s\n\nChange-Id: %s' % (message, change_id)
2333 print(
2334 'WARNING: issue %s has Change-Id footer(s):\n'
2335 ' %s\n'
2336 'but issue has Change-Id %s, according to Gerrit.\n'
2337 'Please, check the proposed correction to the description, '
2338 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2339 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2340 change_id))
2341 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2342 if not options.force:
2343 change_desc = ChangeDescription(message)
2344 change_desc.prompt()
2345 message = change_desc.description
2346 if not message:
2347 DieWithError("Description is empty. Aborting...")
2348 # Continue the while loop.
2349 # Sanity check of this code - we should end up with proper message
2350 # footer.
2351 assert [change_id] == git_footers.get_footer_change_id(message)
2352 change_desc = ChangeDescription(message)
2353 else:
2354 change_desc = ChangeDescription(
2355 options.message or CreateDescriptionFromLog(args))
2356 if not options.force:
2357 change_desc.prompt()
2358 if not change_desc.description:
2359 DieWithError("Description is empty. Aborting...")
2360 message = change_desc.description
2361 change_ids = git_footers.get_footer_change_id(message)
2362 if len(change_ids) > 1:
2363 DieWithError('too many Change-Id footers, at most 1 allowed.')
2364 if not change_ids:
2365 # Generate the Change-Id automatically.
2366 message = git_footers.add_footer_change_id(
2367 message, GenerateGerritChangeId(message))
2368 change_desc.set_description(message)
2369 change_ids = git_footers.get_footer_change_id(message)
2370 assert len(change_ids) == 1
2371 change_id = change_ids[0]
2372
2373 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2374 if remote is '.':
2375 # If our upstream branch is local, we base our squashed commit on its
2376 # squashed version.
2377 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2378 # Check the squashed hash of the parent.
2379 parent = RunGit(['config',
2380 'branch.%s.gerritsquashhash' % upstream_branch_name],
2381 error_ok=True).strip()
2382 # Verify that the upstream branch has been uploaded too, otherwise
2383 # Gerrit will create additional CLs when uploading.
2384 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2385 RunGitSilent(['rev-parse', parent + ':'])):
2386 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2387 DieWithError(
2388 'Upload upstream branch %s first.\n'
2389 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2390 'version of depot_tools. If so, then re-upload it with:\n'
2391 ' git cl upload --squash\n' % upstream_branch_name)
2392 else:
2393 parent = self.GetCommonAncestorWithUpstream()
2394
2395 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2396 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2397 '-m', message]).strip()
2398 else:
2399 change_desc = ChangeDescription(
2400 options.message or CreateDescriptionFromLog(args))
2401 if not change_desc.description:
2402 DieWithError("Description is empty. Aborting...")
2403
2404 if not git_footers.get_footer_change_id(change_desc.description):
2405 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002406 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2407 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002408 ref_to_push = 'HEAD'
2409 parent = '%s/%s' % (gerrit_remote, branch)
2410 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2411
2412 assert change_desc
2413 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2414 ref_to_push)]).splitlines()
2415 if len(commits) > 1:
2416 print('WARNING: This will upload %d commits. Run the following command '
2417 'to see which commits will be uploaded: ' % len(commits))
2418 print('git log %s..%s' % (parent, ref_to_push))
2419 print('You can also use `git squash-branch` to squash these into a '
2420 'single commit.')
2421 ask_for_data('About to upload; enter to confirm.')
2422
2423 if options.reviewers or options.tbr_owners:
2424 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2425 change)
2426
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002427 # Extra options that can be specified at push time. Doc:
2428 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2429 refspec_opts = []
2430 if options.title:
2431 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2432 # reverse on its side.
2433 if '_' in options.title:
2434 print('WARNING: underscores in title will be converted to spaces.')
2435 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2436
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002437 cc = self.GetCCList().split(',')
2438 if options.cc:
2439 cc.extend(options.cc)
2440 cc = filter(None, cc)
2441 if cc:
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002442 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002443
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002444 if change_desc.get_reviewers():
2445 refspec_opts.extend('r=' + email.strip()
2446 for email in change_desc.get_reviewers())
2447
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002448
2449 refspec_suffix = ''
2450 if refspec_opts:
2451 refspec_suffix = '%' + ','.join(refspec_opts)
2452 assert ' ' not in refspec_suffix, (
2453 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002454 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002455
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002456 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002457 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002458 print_stdout=True,
2459 # Flush after every line: useful for seeing progress when running as
2460 # recipe.
2461 filter_fn=lambda _: sys.stdout.flush())
2462
2463 if options.squash:
2464 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2465 change_numbers = [m.group(1)
2466 for m in map(regex.match, push_stdout.splitlines())
2467 if m]
2468 if len(change_numbers) != 1:
2469 DieWithError(
2470 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2471 'Change-Id: %s') % (len(change_numbers), change_id))
2472 self.SetIssue(change_numbers[0])
2473 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2474 ref_to_push])
2475 return 0
2476
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002477 def _AddChangeIdToCommitMessage(self, options, args):
2478 """Re-commits using the current message, assumes the commit hook is in
2479 place.
2480 """
2481 log_desc = options.message or CreateDescriptionFromLog(args)
2482 git_command = ['commit', '--amend', '-m', log_desc]
2483 RunGit(git_command)
2484 new_log_desc = CreateDescriptionFromLog(args)
2485 if git_footers.get_footer_change_id(new_log_desc):
2486 print 'git-cl: Added Change-Id to commit message.'
2487 return new_log_desc
2488 else:
2489 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002490
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002491 def SetCQState(self, new_state):
2492 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2493 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2494 # self-discovery of label config for this CL using REST API.
2495 vote_map = {
2496 _CQState.NONE: 0,
2497 _CQState.DRY_RUN: 1,
2498 _CQState.COMMIT : 2,
2499 }
2500 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2501 labels={'Commit-Queue': vote_map[new_state]})
2502
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002503
2504_CODEREVIEW_IMPLEMENTATIONS = {
2505 'rietveld': _RietveldChangelistImpl,
2506 'gerrit': _GerritChangelistImpl,
2507}
2508
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002509
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002510class ChangeDescription(object):
2511 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002512 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002513 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002514
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002515 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002516 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002517
agable@chromium.org42c20792013-09-12 17:34:49 +00002518 @property # www.logilab.org/ticket/89786
2519 def description(self): # pylint: disable=E0202
2520 return '\n'.join(self._description_lines)
2521
2522 def set_description(self, desc):
2523 if isinstance(desc, basestring):
2524 lines = desc.splitlines()
2525 else:
2526 lines = [line.rstrip() for line in desc]
2527 while lines and not lines[0]:
2528 lines.pop(0)
2529 while lines and not lines[-1]:
2530 lines.pop(-1)
2531 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002532
piman@chromium.org336f9122014-09-04 02:16:55 +00002533 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002534 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002535 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002536 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002537 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002538 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002539
agable@chromium.org42c20792013-09-12 17:34:49 +00002540 # Get the set of R= and TBR= lines and remove them from the desciption.
2541 regexp = re.compile(self.R_LINE)
2542 matches = [regexp.match(line) for line in self._description_lines]
2543 new_desc = [l for i, l in enumerate(self._description_lines)
2544 if not matches[i]]
2545 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002546
agable@chromium.org42c20792013-09-12 17:34:49 +00002547 # Construct new unified R= and TBR= lines.
2548 r_names = []
2549 tbr_names = []
2550 for match in matches:
2551 if not match:
2552 continue
2553 people = cleanup_list([match.group(2).strip()])
2554 if match.group(1) == 'TBR':
2555 tbr_names.extend(people)
2556 else:
2557 r_names.extend(people)
2558 for name in r_names:
2559 if name not in reviewers:
2560 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002561 if add_owners_tbr:
2562 owners_db = owners.Database(change.RepositoryRoot(),
2563 fopen=file, os_path=os.path, glob=glob.glob)
2564 all_reviewers = set(tbr_names + reviewers)
2565 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2566 all_reviewers)
2567 tbr_names.extend(owners_db.reviewers_for(missing_files,
2568 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002569 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2570 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2571
2572 # Put the new lines in the description where the old first R= line was.
2573 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2574 if 0 <= line_loc < len(self._description_lines):
2575 if new_tbr_line:
2576 self._description_lines.insert(line_loc, new_tbr_line)
2577 if new_r_line:
2578 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002579 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002580 if new_r_line:
2581 self.append_footer(new_r_line)
2582 if new_tbr_line:
2583 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002584
2585 def prompt(self):
2586 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002587 self.set_description([
2588 '# Enter a description of the change.',
2589 '# This will be displayed on the codereview site.',
2590 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002591 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002592 '--------------------',
2593 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002594
agable@chromium.org42c20792013-09-12 17:34:49 +00002595 regexp = re.compile(self.BUG_LINE)
2596 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002597 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002598 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002599 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002600 if not content:
2601 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002602 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002603
2604 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002605 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2606 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002607 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002608 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002609
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002610 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002611 if self._description_lines:
2612 # Add an empty line if either the last line or the new line isn't a tag.
2613 last_line = self._description_lines[-1]
2614 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2615 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2616 self._description_lines.append('')
2617 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002618
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002619 def get_reviewers(self):
2620 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002621 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2622 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002623 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002624
2625
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002626def get_approving_reviewers(props):
2627 """Retrieves the reviewers that approved a CL from the issue properties with
2628 messages.
2629
2630 Note that the list may contain reviewers that are not committer, thus are not
2631 considered by the CQ.
2632 """
2633 return sorted(
2634 set(
2635 message['sender']
2636 for message in props['messages']
2637 if message['approval'] and message['sender'] in props['reviewers']
2638 )
2639 )
2640
2641
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002642def FindCodereviewSettingsFile(filename='codereview.settings'):
2643 """Finds the given file starting in the cwd and going up.
2644
2645 Only looks up to the top of the repository unless an
2646 'inherit-review-settings-ok' file exists in the root of the repository.
2647 """
2648 inherit_ok_file = 'inherit-review-settings-ok'
2649 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002650 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002651 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2652 root = '/'
2653 while True:
2654 if filename in os.listdir(cwd):
2655 if os.path.isfile(os.path.join(cwd, filename)):
2656 return open(os.path.join(cwd, filename))
2657 if cwd == root:
2658 break
2659 cwd = os.path.dirname(cwd)
2660
2661
2662def LoadCodereviewSettingsFromFile(fileobj):
2663 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002664 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002665
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002666 def SetProperty(name, setting, unset_error_ok=False):
2667 fullname = 'rietveld.' + name
2668 if setting in keyvals:
2669 RunGit(['config', fullname, keyvals[setting]])
2670 else:
2671 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2672
2673 SetProperty('server', 'CODE_REVIEW_SERVER')
2674 # Only server setting is required. Other settings can be absent.
2675 # In that case, we ignore errors raised during option deletion attempt.
2676 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002677 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002678 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2679 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002680 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002681 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002682 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2683 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002684 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002685 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002686 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002687 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2688 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002690 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002691 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002692
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002693 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2694 RunGit(['config', 'gerrit.squash-uploads',
2695 keyvals['GERRIT_SQUASH_UPLOADS']])
2696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002697 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2698 #should be of the form
2699 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2700 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2701 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2702 keyvals['ORIGIN_URL_CONFIG']])
2703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002704
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002705def urlretrieve(source, destination):
2706 """urllib is broken for SSL connections via a proxy therefore we
2707 can't use urllib.urlretrieve()."""
2708 with open(destination, 'w') as f:
2709 f.write(urllib2.urlopen(source).read())
2710
2711
ukai@chromium.org712d6102013-11-27 00:52:58 +00002712def hasSheBang(fname):
2713 """Checks fname is a #! script."""
2714 with open(fname) as f:
2715 return f.read(2).startswith('#!')
2716
2717
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002718# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2719def DownloadHooks(*args, **kwargs):
2720 pass
2721
2722
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002723def DownloadGerritHook(force):
2724 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002725
2726 Args:
2727 force: True to update hooks. False to install hooks if not present.
2728 """
2729 if not settings.GetIsGerrit():
2730 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002731 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002732 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2733 if not os.access(dst, os.X_OK):
2734 if os.path.exists(dst):
2735 if not force:
2736 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002737 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002738 print(
2739 'WARNING: installing Gerrit commit-msg hook.\n'
2740 ' This behavior of git cl will soon be disabled.\n'
2741 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002742 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002743 if not hasSheBang(dst):
2744 DieWithError('Not a script: %s\n'
2745 'You need to download from\n%s\n'
2746 'into .git/hooks/commit-msg and '
2747 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002748 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2749 except Exception:
2750 if os.path.exists(dst):
2751 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002752 DieWithError('\nFailed to download hooks.\n'
2753 'You need to download from\n%s\n'
2754 'into .git/hooks/commit-msg and '
2755 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002756
2757
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002758
2759def GetRietveldCodereviewSettingsInteractively():
2760 """Prompt the user for settings."""
2761 server = settings.GetDefaultServerUrl(error_ok=True)
2762 prompt = 'Rietveld server (host[:port])'
2763 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2764 newserver = ask_for_data(prompt + ':')
2765 if not server and not newserver:
2766 newserver = DEFAULT_SERVER
2767 if newserver:
2768 newserver = gclient_utils.UpgradeToHttps(newserver)
2769 if newserver != server:
2770 RunGit(['config', 'rietveld.server', newserver])
2771
2772 def SetProperty(initial, caption, name, is_url):
2773 prompt = caption
2774 if initial:
2775 prompt += ' ("x" to clear) [%s]' % initial
2776 new_val = ask_for_data(prompt + ':')
2777 if new_val == 'x':
2778 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2779 elif new_val:
2780 if is_url:
2781 new_val = gclient_utils.UpgradeToHttps(new_val)
2782 if new_val != initial:
2783 RunGit(['config', 'rietveld.' + name, new_val])
2784
2785 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2786 SetProperty(settings.GetDefaultPrivateFlag(),
2787 'Private flag (rietveld only)', 'private', False)
2788 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2789 'tree-status-url', False)
2790 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2791 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2792 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2793 'run-post-upload-hook', False)
2794
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002795@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002796def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002797 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002798
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002799 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002800 'For Gerrit, see http://crbug.com/603116.')
2801 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002802 parser.add_option('--activate-update', action='store_true',
2803 help='activate auto-updating [rietveld] section in '
2804 '.git/config')
2805 parser.add_option('--deactivate-update', action='store_true',
2806 help='deactivate auto-updating [rietveld] section in '
2807 '.git/config')
2808 options, args = parser.parse_args(args)
2809
2810 if options.deactivate_update:
2811 RunGit(['config', 'rietveld.autoupdate', 'false'])
2812 return
2813
2814 if options.activate_update:
2815 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2816 return
2817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002818 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002819 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820 return 0
2821
2822 url = args[0]
2823 if not url.endswith('codereview.settings'):
2824 url = os.path.join(url, 'codereview.settings')
2825
2826 # Load code review settings and download hooks (if available).
2827 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2828 return 0
2829
2830
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002831def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002832 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002833 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2834 branch = ShortBranchName(branchref)
2835 _, args = parser.parse_args(args)
2836 if not args:
2837 print("Current base-url:")
2838 return RunGit(['config', 'branch.%s.base-url' % branch],
2839 error_ok=False).strip()
2840 else:
2841 print("Setting base-url to %s" % args[0])
2842 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2843 error_ok=False).strip()
2844
2845
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002846def color_for_status(status):
2847 """Maps a Changelist status to color, for CMDstatus and other tools."""
2848 return {
2849 'unsent': Fore.RED,
2850 'waiting': Fore.BLUE,
2851 'reply': Fore.YELLOW,
2852 'lgtm': Fore.GREEN,
2853 'commit': Fore.MAGENTA,
2854 'closed': Fore.CYAN,
2855 'error': Fore.WHITE,
2856 }.get(status, Fore.WHITE)
2857
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002858def fetch_cl_status(branch, auth_config=None):
2859 """Fetches information for an issue and returns (branch, issue, status)."""
2860 cl = Changelist(branchref=branch, auth_config=auth_config)
2861 url = cl.GetIssueURL()
2862 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002863
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002864 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002865 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002866 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002867
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002868 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002869
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002870def get_cl_statuses(
2871 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002872 """Returns a blocking iterable of (branch, issue, color) for given branches.
2873
2874 If fine_grained is true, this will fetch CL statuses from the server.
2875 Otherwise, simply indicate if there's a matching url for the given branches.
2876
2877 If max_processes is specified, it is used as the maximum number of processes
2878 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2879 spawned.
2880 """
2881 # Silence upload.py otherwise it becomes unwieldly.
2882 upload.verbosity = 0
2883
2884 if fine_grained:
2885 # Process one branch synchronously to work through authentication, then
2886 # spawn processes to process all the other branches in parallel.
2887 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002888 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2889 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002890
2891 branches_to_fetch = branches[1:]
2892 pool = ThreadPool(
2893 min(max_processes, len(branches_to_fetch))
2894 if max_processes is not None
2895 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002896 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002897 yield x
2898 else:
2899 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2900 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002901 cl = Changelist(branchref=b, auth_config=auth_config)
2902 url = cl.GetIssueURL()
2903 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002904
rmistry@google.com2dd99862015-06-22 12:22:18 +00002905
2906def upload_branch_deps(cl, args):
2907 """Uploads CLs of local branches that are dependents of the current branch.
2908
2909 If the local branch dependency tree looks like:
2910 test1 -> test2.1 -> test3.1
2911 -> test3.2
2912 -> test2.2 -> test3.3
2913
2914 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2915 run on the dependent branches in this order:
2916 test2.1, test3.1, test3.2, test2.2, test3.3
2917
2918 Note: This function does not rebase your local dependent branches. Use it when
2919 you make a change to the parent branch that will not conflict with its
2920 dependent branches, and you would like their dependencies updated in
2921 Rietveld.
2922 """
2923 if git_common.is_dirty_git_tree('upload-branch-deps'):
2924 return 1
2925
2926 root_branch = cl.GetBranch()
2927 if root_branch is None:
2928 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2929 'Get on a branch!')
2930 if not cl.GetIssue() or not cl.GetPatchset():
2931 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2932 'patchset dependencies without an uploaded CL.')
2933
2934 branches = RunGit(['for-each-ref',
2935 '--format=%(refname:short) %(upstream:short)',
2936 'refs/heads'])
2937 if not branches:
2938 print('No local branches found.')
2939 return 0
2940
2941 # Create a dictionary of all local branches to the branches that are dependent
2942 # on it.
2943 tracked_to_dependents = collections.defaultdict(list)
2944 for b in branches.splitlines():
2945 tokens = b.split()
2946 if len(tokens) == 2:
2947 branch_name, tracked = tokens
2948 tracked_to_dependents[tracked].append(branch_name)
2949
2950 print
2951 print 'The dependent local branches of %s are:' % root_branch
2952 dependents = []
2953 def traverse_dependents_preorder(branch, padding=''):
2954 dependents_to_process = tracked_to_dependents.get(branch, [])
2955 padding += ' '
2956 for dependent in dependents_to_process:
2957 print '%s%s' % (padding, dependent)
2958 dependents.append(dependent)
2959 traverse_dependents_preorder(dependent, padding)
2960 traverse_dependents_preorder(root_branch)
2961 print
2962
2963 if not dependents:
2964 print 'There are no dependent local branches for %s' % root_branch
2965 return 0
2966
2967 print ('This command will checkout all dependent branches and run '
2968 '"git cl upload".')
2969 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2970
andybons@chromium.org962f9462016-02-03 20:00:42 +00002971 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002972 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002973 args.extend(['-t', 'Updated patchset dependency'])
2974
rmistry@google.com2dd99862015-06-22 12:22:18 +00002975 # Record all dependents that failed to upload.
2976 failures = {}
2977 # Go through all dependents, checkout the branch and upload.
2978 try:
2979 for dependent_branch in dependents:
2980 print
2981 print '--------------------------------------'
2982 print 'Running "git cl upload" from %s:' % dependent_branch
2983 RunGit(['checkout', '-q', dependent_branch])
2984 print
2985 try:
2986 if CMDupload(OptionParser(), args) != 0:
2987 print 'Upload failed for %s!' % dependent_branch
2988 failures[dependent_branch] = 1
2989 except: # pylint: disable=W0702
2990 failures[dependent_branch] = 1
2991 print
2992 finally:
2993 # Swap back to the original root branch.
2994 RunGit(['checkout', '-q', root_branch])
2995
2996 print
2997 print 'Upload complete for dependent branches!'
2998 for dependent_branch in dependents:
2999 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3000 print ' %s : %s' % (dependent_branch, upload_status)
3001 print
3002
3003 return 0
3004
3005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003006def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003007 """Show status of changelists.
3008
3009 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003010 - Red not sent for review or broken
3011 - Blue waiting for review
3012 - Yellow waiting for you to reply to review
3013 - Green LGTM'ed
3014 - Magenta in the commit queue
3015 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003016
3017 Also see 'git cl comments'.
3018 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003019 parser.add_option('--field',
3020 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003021 parser.add_option('-f', '--fast', action='store_true',
3022 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003023 parser.add_option(
3024 '-j', '--maxjobs', action='store', type=int,
3025 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003026
3027 auth.add_auth_options(parser)
3028 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003029 if args:
3030 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003031 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003033 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003034 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003035 if options.field.startswith('desc'):
3036 print cl.GetDescription()
3037 elif options.field == 'id':
3038 issueid = cl.GetIssue()
3039 if issueid:
3040 print issueid
3041 elif options.field == 'patch':
3042 patchset = cl.GetPatchset()
3043 if patchset:
3044 print patchset
3045 elif options.field == 'url':
3046 url = cl.GetIssueURL()
3047 if url:
3048 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003049 return 0
3050
3051 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3052 if not branches:
3053 print('No local branch found.')
3054 return 0
3055
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003056 changes = (
3057 Changelist(branchref=b, auth_config=auth_config)
3058 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003059 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00003060 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003061 alignment = max(5, max(len(b) for b in branches))
3062 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003063 output = get_cl_statuses(branches,
3064 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003065 max_processes=options.maxjobs,
3066 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003067
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003068 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003069 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003070 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003071 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003072 b, i, status = output.next()
3073 branch_statuses[b] = (i, status)
3074 issue_url, status = branch_statuses.pop(branch)
3075 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003076 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003077 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003078 color = ''
3079 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003080 status_str = '(%s)' % status if status else ''
3081 print ' %*s : %s%s %s%s' % (
3082 alignment, ShortBranchName(branch), color, issue_url, status_str,
3083 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003084
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003085 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003086 print
3087 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003088 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003089 if not cl.GetIssue():
3090 print 'No issue assigned.'
3091 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003092 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003093 if not options.fast:
3094 print 'Issue description:'
3095 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003096 return 0
3097
3098
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003099def colorize_CMDstatus_doc():
3100 """To be called once in main() to add colors to git cl status help."""
3101 colors = [i for i in dir(Fore) if i[0].isupper()]
3102
3103 def colorize_line(line):
3104 for color in colors:
3105 if color in line.upper():
3106 # Extract whitespaces first and the leading '-'.
3107 indent = len(line) - len(line.lstrip(' ')) + 1
3108 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3109 return line
3110
3111 lines = CMDstatus.__doc__.splitlines()
3112 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3113
3114
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003115@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003116def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003117 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003118
3119 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003120 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003121 parser.add_option('-r', '--reverse', action='store_true',
3122 help='Lookup the branch(es) for the specified issues. If '
3123 'no issues are specified, all branches with mapped '
3124 'issues will be listed.')
3125 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003126
dnj@chromium.org406c4402015-03-03 17:22:28 +00003127 if options.reverse:
3128 branches = RunGit(['for-each-ref', 'refs/heads',
3129 '--format=%(refname:short)']).splitlines()
3130
3131 # Reverse issue lookup.
3132 issue_branch_map = {}
3133 for branch in branches:
3134 cl = Changelist(branchref=branch)
3135 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3136 if not args:
3137 args = sorted(issue_branch_map.iterkeys())
3138 for issue in args:
3139 if not issue:
3140 continue
3141 print 'Branch for issue number %s: %s' % (
3142 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3143 else:
3144 cl = Changelist()
3145 if len(args) > 0:
3146 try:
3147 issue = int(args[0])
3148 except ValueError:
3149 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003150 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003151 cl.SetIssue(issue)
3152 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003153 return 0
3154
3155
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003156def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003157 """Shows or posts review comments for any changelist."""
3158 parser.add_option('-a', '--add-comment', dest='comment',
3159 help='comment to add to an issue')
3160 parser.add_option('-i', dest='issue',
3161 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003162 parser.add_option('-j', '--json-file',
3163 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003164 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003165 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003166 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003167
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003168 issue = None
3169 if options.issue:
3170 try:
3171 issue = int(options.issue)
3172 except ValueError:
3173 DieWithError('A review issue id is expected to be a number')
3174
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003175 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003176
3177 if options.comment:
3178 cl.AddComment(options.comment)
3179 return 0
3180
3181 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003182 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003183 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003184 summary.append({
3185 'date': message['date'],
3186 'lgtm': False,
3187 'message': message['text'],
3188 'not_lgtm': False,
3189 'sender': message['sender'],
3190 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003191 if message['disapproval']:
3192 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003193 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003194 elif message['approval']:
3195 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003196 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003197 elif message['sender'] == data['owner_email']:
3198 color = Fore.MAGENTA
3199 else:
3200 color = Fore.BLUE
3201 print '\n%s%s %s%s' % (
3202 color, message['date'].split('.', 1)[0], message['sender'],
3203 Fore.RESET)
3204 if message['text'].strip():
3205 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003206 if options.json_file:
3207 with open(options.json_file, 'wb') as f:
3208 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003209 return 0
3210
3211
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003212def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003213 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003214 parser.add_option('-d', '--display', action='store_true',
3215 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003216 auth.add_auth_options(parser)
3217 options, _ = parser.parse_args(args)
3218 auth_config = auth.extract_auth_config_from_options(options)
3219 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003220 if not cl.GetIssue():
3221 DieWithError('This branch has no associated changelist.')
3222 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003223 if options.display:
3224 print description.description
3225 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003226 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003227 if cl.GetDescription() != description.description:
3228 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003229 return 0
3230
3231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232def CreateDescriptionFromLog(args):
3233 """Pulls out the commit log to use as a base for the CL description."""
3234 log_args = []
3235 if len(args) == 1 and not args[0].endswith('.'):
3236 log_args = [args[0] + '..']
3237 elif len(args) == 1 and args[0].endswith('...'):
3238 log_args = [args[0][:-1]]
3239 elif len(args) == 2:
3240 log_args = [args[0] + '..' + args[1]]
3241 else:
3242 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003243 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003244
3245
thestig@chromium.org44202a22014-03-11 19:22:18 +00003246def CMDlint(parser, args):
3247 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003248 parser.add_option('--filter', action='append', metavar='-x,+y',
3249 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003250 auth.add_auth_options(parser)
3251 options, args = parser.parse_args(args)
3252 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003253
3254 # Access to a protected member _XX of a client class
3255 # pylint: disable=W0212
3256 try:
3257 import cpplint
3258 import cpplint_chromium
3259 except ImportError:
3260 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3261 return 1
3262
3263 # Change the current working directory before calling lint so that it
3264 # shows the correct base.
3265 previous_cwd = os.getcwd()
3266 os.chdir(settings.GetRoot())
3267 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003268 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003269 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3270 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003271 if not files:
3272 print "Cannot lint an empty CL"
3273 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003274
3275 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003276 command = args + files
3277 if options.filter:
3278 command = ['--filter=' + ','.join(options.filter)] + command
3279 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003280
3281 white_regex = re.compile(settings.GetLintRegex())
3282 black_regex = re.compile(settings.GetLintIgnoreRegex())
3283 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3284 for filename in filenames:
3285 if white_regex.match(filename):
3286 if black_regex.match(filename):
3287 print "Ignoring file %s" % filename
3288 else:
3289 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3290 extra_check_functions)
3291 else:
3292 print "Skipping file %s" % filename
3293 finally:
3294 os.chdir(previous_cwd)
3295 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3296 if cpplint._cpplint_state.error_count != 0:
3297 return 1
3298 return 0
3299
3300
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003301def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003302 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003303 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003305 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003306 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003307 auth.add_auth_options(parser)
3308 options, args = parser.parse_args(args)
3309 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003310
sbc@chromium.org71437c02015-04-09 19:29:40 +00003311 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003312 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003313 return 1
3314
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003315 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003316 if args:
3317 base_branch = args[0]
3318 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003319 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003320 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003321
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003322 cl.RunHook(
3323 committing=not options.upload,
3324 may_prompt=False,
3325 verbose=options.verbose,
3326 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003327 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003328
3329
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003330def GenerateGerritChangeId(message):
3331 """Returns Ixxxxxx...xxx change id.
3332
3333 Works the same way as
3334 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3335 but can be called on demand on all platforms.
3336
3337 The basic idea is to generate git hash of a state of the tree, original commit
3338 message, author/committer info and timestamps.
3339 """
3340 lines = []
3341 tree_hash = RunGitSilent(['write-tree'])
3342 lines.append('tree %s' % tree_hash.strip())
3343 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3344 if code == 0:
3345 lines.append('parent %s' % parent.strip())
3346 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3347 lines.append('author %s' % author.strip())
3348 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3349 lines.append('committer %s' % committer.strip())
3350 lines.append('')
3351 # Note: Gerrit's commit-hook actually cleans message of some lines and
3352 # whitespace. This code is not doing this, but it clearly won't decrease
3353 # entropy.
3354 lines.append(message)
3355 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3356 stdin='\n'.join(lines))
3357 return 'I%s' % change_hash.strip()
3358
3359
wittman@chromium.org455dc922015-01-26 20:15:50 +00003360def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3361 """Computes the remote branch ref to use for the CL.
3362
3363 Args:
3364 remote (str): The git remote for the CL.
3365 remote_branch (str): The git remote branch for the CL.
3366 target_branch (str): The target branch specified by the user.
3367 pending_prefix (str): The pending prefix from the settings.
3368 """
3369 if not (remote and remote_branch):
3370 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003371
wittman@chromium.org455dc922015-01-26 20:15:50 +00003372 if target_branch:
3373 # Cannonicalize branch references to the equivalent local full symbolic
3374 # refs, which are then translated into the remote full symbolic refs
3375 # below.
3376 if '/' not in target_branch:
3377 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3378 else:
3379 prefix_replacements = (
3380 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3381 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3382 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3383 )
3384 match = None
3385 for regex, replacement in prefix_replacements:
3386 match = re.search(regex, target_branch)
3387 if match:
3388 remote_branch = target_branch.replace(match.group(0), replacement)
3389 break
3390 if not match:
3391 # This is a branch path but not one we recognize; use as-is.
3392 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003393 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3394 # Handle the refs that need to land in different refs.
3395 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003396
wittman@chromium.org455dc922015-01-26 20:15:50 +00003397 # Create the true path to the remote branch.
3398 # Does the following translation:
3399 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3400 # * refs/remotes/origin/master -> refs/heads/master
3401 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3402 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3403 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3404 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3405 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3406 'refs/heads/')
3407 elif remote_branch.startswith('refs/remotes/branch-heads'):
3408 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3409 # If a pending prefix exists then replace refs/ with it.
3410 if pending_prefix:
3411 remote_branch = remote_branch.replace('refs/', pending_prefix)
3412 return remote_branch
3413
3414
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003415def cleanup_list(l):
3416 """Fixes a list so that comma separated items are put as individual items.
3417
3418 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3419 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3420 """
3421 items = sum((i.split(',') for i in l), [])
3422 stripped_items = (i.strip() for i in items)
3423 return sorted(filter(None, stripped_items))
3424
3425
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003426@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003427def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003428 """Uploads the current changelist to codereview.
3429
3430 Can skip dependency patchset uploads for a branch by running:
3431 git config branch.branch_name.skip-deps-uploads True
3432 To unset run:
3433 git config --unset branch.branch_name.skip-deps-uploads
3434 Can also set the above globally by using the --global flag.
3435 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003436 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3437 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003438 parser.add_option('--bypass-watchlists', action='store_true',
3439 dest='bypass_watchlists',
3440 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003441 parser.add_option('-f', action='store_true', dest='force',
3442 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003443 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003444 parser.add_option('-t', dest='title',
3445 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003446 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003447 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003448 help='reviewer email addresses')
3449 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003450 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003451 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003452 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003453 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003454 parser.add_option('--emulate_svn_auto_props',
3455 '--emulate-svn-auto-props',
3456 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003457 dest="emulate_svn_auto_props",
3458 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003459 parser.add_option('-c', '--use-commit-queue', action='store_true',
3460 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003461 parser.add_option('--private', action='store_true',
3462 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003463 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003464 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003465 metavar='TARGET',
3466 help='Apply CL to remote ref TARGET. ' +
3467 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003468 parser.add_option('--squash', action='store_true',
3469 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003470 parser.add_option('--no-squash', action='store_true',
3471 help='Don\'t squash multiple commits into one ' +
3472 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003473 parser.add_option('--email', default=None,
3474 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003475 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3476 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003477 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3478 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003479 help='Send the patchset to do a CQ dry run right after '
3480 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003481 parser.add_option('--dependencies', action='store_true',
3482 help='Uploads CLs of all the local branches that depend on '
3483 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003484
rmistry@google.com2dd99862015-06-22 12:22:18 +00003485 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003486 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003487 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003488 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003489 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003490
sbc@chromium.org71437c02015-04-09 19:29:40 +00003491 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003492 return 1
3493
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003494 options.reviewers = cleanup_list(options.reviewers)
3495 options.cc = cleanup_list(options.cc)
3496
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003497 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3498 settings.GetIsGerrit()
3499
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003500 cl = Changelist(auth_config=auth_config)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003501 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003502
3503
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003504def IsSubmoduleMergeCommit(ref):
3505 # When submodules are added to the repo, we expect there to be a single
3506 # non-git-svn merge commit at remote HEAD with a signature comment.
3507 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003508 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003509 return RunGit(cmd) != ''
3510
3511
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003512def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003513 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003514
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003515 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3516 upstream and closes the issue automatically and atomically.
3517
3518 Otherwise (in case of Rietveld):
3519 Squashes branch into a single commit.
3520 Updates changelog with metadata (e.g. pointer to review).
3521 Pushes/dcommits the code upstream.
3522 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523 """
3524 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3525 help='bypass upload presubmit hook')
3526 parser.add_option('-m', dest='message',
3527 help="override review description")
3528 parser.add_option('-f', action='store_true', dest='force',
3529 help="force yes to questions (don't prompt)")
3530 parser.add_option('-c', dest='contributor',
3531 help="external contributor for patch (appended to " +
3532 "description and used as author for git). Should be " +
3533 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003534 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003535 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003536 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003537 auth_config = auth.extract_auth_config_from_options(options)
3538
3539 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003540
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003541 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3542 if cl.IsGerrit():
3543 if options.message:
3544 # This could be implemented, but it requires sending a new patch to
3545 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3546 # Besides, Gerrit has the ability to change the commit message on submit
3547 # automatically, thus there is no need to support this option (so far?).
3548 parser.error('-m MESSAGE option is not supported for Gerrit.')
3549 if options.contributor:
3550 parser.error(
3551 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3552 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3553 'the contributor\'s "name <email>". If you can\'t upload such a '
3554 'commit for review, contact your repository admin and request'
3555 '"Forge-Author" permission.')
3556 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3557 options.verbose)
3558
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003559 current = cl.GetBranch()
3560 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3561 if not settings.GetIsGitSvn() and remote == '.':
3562 print
3563 print 'Attempting to push branch %r into another local branch!' % current
3564 print
3565 print 'Either reparent this branch on top of origin/master:'
3566 print ' git reparent-branch --root'
3567 print
3568 print 'OR run `git rebase-update` if you think the parent branch is already'
3569 print 'committed.'
3570 print
3571 print ' Current parent: %r' % upstream_branch
3572 return 1
3573
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003574 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003575 # Default to merging against our best guess of the upstream branch.
3576 args = [cl.GetUpstreamBranch()]
3577
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003578 if options.contributor:
3579 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3580 print "Please provide contibutor as 'First Last <email@example.com>'"
3581 return 1
3582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003584 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003585
sbc@chromium.org71437c02015-04-09 19:29:40 +00003586 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587 return 1
3588
3589 # This rev-list syntax means "show all commits not in my branch that
3590 # are in base_branch".
3591 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3592 base_branch]).splitlines()
3593 if upstream_commits:
3594 print ('Base branch "%s" has %d commits '
3595 'not in this branch.' % (base_branch, len(upstream_commits)))
3596 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3597 return 1
3598
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003599 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003600 svn_head = None
3601 if cmd == 'dcommit' or base_has_submodules:
3602 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3603 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003605 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003606 # If the base_head is a submodule merge commit, the first parent of the
3607 # base_head should be a git-svn commit, which is what we're interested in.
3608 base_svn_head = base_branch
3609 if base_has_submodules:
3610 base_svn_head += '^1'
3611
3612 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003613 if extra_commits:
3614 print ('This branch has %d additional commits not upstreamed yet.'
3615 % len(extra_commits.splitlines()))
3616 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3617 'before attempting to %s.' % (base_branch, cmd))
3618 return 1
3619
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003620 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003621 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003622 author = None
3623 if options.contributor:
3624 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003625 hook_results = cl.RunHook(
3626 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003627 may_prompt=not options.force,
3628 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003629 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003630 if not hook_results.should_continue():
3631 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003633 # Check the tree status if the tree status URL is set.
3634 status = GetTreeStatus()
3635 if 'closed' == status:
3636 print('The tree is closed. Please wait for it to reopen. Use '
3637 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3638 return 1
3639 elif 'unknown' == status:
3640 print('Unable to determine tree status. Please verify manually and '
3641 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3642 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003643
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003644 change_desc = ChangeDescription(options.message)
3645 if not change_desc.description and cl.GetIssue():
3646 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003647
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003648 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003649 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003650 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003651 else:
3652 print 'No description set.'
3653 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3654 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003655
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003656 # Keep a separate copy for the commit message, because the commit message
3657 # contains the link to the Rietveld issue, while the Rietveld message contains
3658 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003659 # Keep a separate copy for the commit message.
3660 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003661 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003662
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003663 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003664 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003665 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003666 # after it. Add a period on a new line to circumvent this. Also add a space
3667 # before the period to make sure that Gitiles continues to correctly resolve
3668 # the URL.
3669 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003670 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003671 commit_desc.append_footer('Patch from %s.' % options.contributor)
3672
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003673 print('Description:')
3674 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003675
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003676 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003677 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003678 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003679
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003680 # We want to squash all this branch's commits into one commit with the proper
3681 # description. We do this by doing a "reset --soft" to the base branch (which
3682 # keeps the working copy the same), then dcommitting that. If origin/master
3683 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3684 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003685 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003686 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3687 # Delete the branches if they exist.
3688 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3689 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3690 result = RunGitWithCode(showref_cmd)
3691 if result[0] == 0:
3692 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003693
3694 # We might be in a directory that's present in this branch but not in the
3695 # trunk. Move up to the top of the tree so that git commands that expect a
3696 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003697 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003698 if rel_base_path:
3699 os.chdir(rel_base_path)
3700
3701 # Stuff our change into the merge branch.
3702 # We wrap in a try...finally block so if anything goes wrong,
3703 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003704 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003705 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003706 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003707 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003708 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003709 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003710 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003712 RunGit(
3713 [
3714 'commit', '--author', options.contributor,
3715 '-m', commit_desc.description,
3716 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003718 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003719 if base_has_submodules:
3720 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3721 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3722 RunGit(['checkout', CHERRY_PICK_BRANCH])
3723 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003724 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003725 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003726 mirror = settings.GetGitMirror(remote)
3727 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003728 pending_prefix = settings.GetPendingRefPrefix()
3729 if not pending_prefix or branch.startswith(pending_prefix):
3730 # If not using refs/pending/heads/* at all, or target ref is already set
3731 # to pending, then push to the target ref directly.
3732 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003733 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003734 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003735 else:
3736 # Cherry-pick the change on top of pending ref and then push it.
3737 assert branch.startswith('refs/'), branch
3738 assert pending_prefix[-1] == '/', pending_prefix
3739 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003740 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003741 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003742 if retcode == 0:
3743 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744 else:
3745 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003746 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003747 'svn', 'dcommit',
3748 '-C%s' % options.similarity,
3749 '--no-rebase', '--rmdir',
3750 ]
3751 if settings.GetForceHttpsCommitUrl():
3752 # Allow forcing https commit URLs for some projects that don't allow
3753 # committing to http URLs (like Google Code).
3754 remote_url = cl.GetGitSvnRemoteUrl()
3755 if urlparse.urlparse(remote_url).scheme == 'http':
3756 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003757 cmd_args.append('--commit-url=%s' % remote_url)
3758 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003759 if 'Committed r' in output:
3760 revision = re.match(
3761 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3762 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003763 finally:
3764 # And then swap back to the original branch and clean up.
3765 RunGit(['checkout', '-q', cl.GetBranch()])
3766 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003767 if base_has_submodules:
3768 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003769
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003770 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003771 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003772 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003773
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003774 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003775 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003776 try:
3777 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3778 # We set pushed_to_pending to False, since it made it all the way to the
3779 # real ref.
3780 pushed_to_pending = False
3781 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003782 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003785 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003787 if not to_pending:
3788 if viewvc_url and revision:
3789 change_desc.append_footer(
3790 'Committed: %s%s' % (viewvc_url, revision))
3791 elif revision:
3792 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003793 print ('Closing issue '
3794 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003795 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003796 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003797 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003798 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003799 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003800 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003801 if options.bypass_hooks:
3802 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3803 else:
3804 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003805 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003806 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003807
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003808 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003809 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3810 print 'The commit is in the pending queue (%s).' % pending_ref
3811 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003812 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003813 'footer.' % branch)
3814
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003815 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3816 if os.path.isfile(hook):
3817 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003818
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003819 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820
3821
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003822def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3823 print
3824 print 'Waiting for commit to be landed on %s...' % real_ref
3825 print '(If you are impatient, you may Ctrl-C once without harm)'
3826 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3827 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003828 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003829
3830 loop = 0
3831 while True:
3832 sys.stdout.write('fetching (%d)... \r' % loop)
3833 sys.stdout.flush()
3834 loop += 1
3835
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003836 if mirror:
3837 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003838 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3839 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3840 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3841 for commit in commits.splitlines():
3842 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3843 print 'Found commit on %s' % real_ref
3844 return commit
3845
3846 current_rev = to_rev
3847
3848
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003849def PushToGitPending(remote, pending_ref, upstream_ref):
3850 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3851
3852 Returns:
3853 (retcode of last operation, output log of last operation).
3854 """
3855 assert pending_ref.startswith('refs/'), pending_ref
3856 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3857 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3858 code = 0
3859 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003860 max_attempts = 3
3861 attempts_left = max_attempts
3862 while attempts_left:
3863 if attempts_left != max_attempts:
3864 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3865 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003866
3867 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003868 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003869 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003870 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003871 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003872 print 'Fetch failed with exit code %d.' % code
3873 if out.strip():
3874 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003875 continue
3876
3877 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003878 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003879 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003880 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003881 if code:
3882 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003883 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3884 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003885 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3886 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003887 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003888 return code, out
3889
3890 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003891 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003892 code, out = RunGitWithCode(
3893 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3894 if code == 0:
3895 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003896 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003897 return code, out
3898
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003899 print 'Push failed with exit code %d.' % code
3900 if out.strip():
3901 print out.strip()
3902 if IsFatalPushFailure(out):
3903 print (
3904 'Fatal push error. Make sure your .netrc credentials and git '
3905 'user.email are correct and you have push access to the repo.')
3906 return code, out
3907
3908 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003909 return code, out
3910
3911
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003912def IsFatalPushFailure(push_stdout):
3913 """True if retrying push won't help."""
3914 return '(prohibited by Gerrit)' in push_stdout
3915
3916
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003917@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003918def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003919 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003920 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003921 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003922 # If it looks like previous commits were mirrored with git-svn.
3923 message = """This repository appears to be a git-svn mirror, but no
3924upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3925 else:
3926 message = """This doesn't appear to be an SVN repository.
3927If your project has a true, writeable git repository, you probably want to run
3928'git cl land' instead.
3929If your project has a git mirror of an upstream SVN master, you probably need
3930to run 'git svn init'.
3931
3932Using the wrong command might cause your commit to appear to succeed, and the
3933review to be closed, without actually landing upstream. If you choose to
3934proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003935 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003936 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003937 return SendUpstream(parser, args, 'dcommit')
3938
3939
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003940@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003941def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003942 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003943 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003944 print('This appears to be an SVN repository.')
3945 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003946 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003947 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003948 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949
3950
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003951@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003952def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003953 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003954 parser.add_option('-b', dest='newbranch',
3955 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003956 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003957 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003958 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3959 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003960 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003961 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003962 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003963 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003965 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003966
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003967
3968 group = optparse.OptionGroup(
3969 parser,
3970 'Options for continuing work on the current issue uploaded from a '
3971 'different clone (e.g. different machine). Must be used independently '
3972 'from the other options. No issue number should be specified, and the '
3973 'branch must have an issue number associated with it')
3974 group.add_option('--reapply', action='store_true', dest='reapply',
3975 help='Reset the branch and reapply the issue.\n'
3976 'CAUTION: This will undo any local changes in this '
3977 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003978
3979 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003980 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003981 parser.add_option_group(group)
3982
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003983 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003985 auth_config = auth.extract_auth_config_from_options(options)
3986
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003987 cl = Changelist(auth_config=auth_config)
3988
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003989 issue_arg = None
3990 if options.reapply :
3991 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003992 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003993
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003994 issue_arg = cl.GetIssue()
3995 upstream = cl.GetUpstreamBranch()
3996 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003997 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003998
3999 RunGit(['reset', '--hard', upstream])
4000 if options.pull:
4001 RunGit(['pull'])
4002 else:
4003 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004004 parser.error('Must specify issue number or url')
4005 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004006
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004007 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004008 parser.print_help()
4009 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004011 if cl.IsGerrit():
4012 if options.reject:
4013 parser.error('--reject is not supported with Gerrit codereview.')
4014 if options.nocommit:
4015 parser.error('--nocommit is not supported with Gerrit codereview.')
4016 if options.directory:
4017 parser.error('--directory is not supported with Gerrit codereview.')
4018
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004019 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004020 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004021 return 1
4022
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004023 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004024 if options.reapply:
4025 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004026 if options.force:
4027 RunGit(['branch', '-D', options.newbranch],
4028 stderr=subprocess2.PIPE, error_ok=True)
4029 RunGit(['checkout', '-b', options.newbranch,
4030 Changelist().GetUpstreamBranch()])
4031
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004032 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4033 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004034
4035
4036def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004037 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004038 # Provide a wrapper for git svn rebase to help avoid accidental
4039 # git svn dcommit.
4040 # It's the only command that doesn't use parser at all since we just defer
4041 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004042
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004043 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044
4045
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004046def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004047 """Fetches the tree status and returns either 'open', 'closed',
4048 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004049 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050 if url:
4051 status = urllib2.urlopen(url).read().lower()
4052 if status.find('closed') != -1 or status == '0':
4053 return 'closed'
4054 elif status.find('open') != -1 or status == '1':
4055 return 'open'
4056 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004057 return 'unset'
4058
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004060def GetTreeStatusReason():
4061 """Fetches the tree status from a json url and returns the message
4062 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004063 url = settings.GetTreeStatusUrl()
4064 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065 connection = urllib2.urlopen(json_url)
4066 status = json.loads(connection.read())
4067 connection.close()
4068 return status['message']
4069
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004070
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004071def GetBuilderMaster(bot_list):
4072 """For a given builder, fetch the master from AE if available."""
4073 map_url = 'https://builders-map.appspot.com/'
4074 try:
4075 master_map = json.load(urllib2.urlopen(map_url))
4076 except urllib2.URLError as e:
4077 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4078 (map_url, e))
4079 except ValueError as e:
4080 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4081 if not master_map:
4082 return None, 'Failed to build master map.'
4083
4084 result_master = ''
4085 for bot in bot_list:
4086 builder = bot.split(':', 1)[0]
4087 master_list = master_map.get(builder, [])
4088 if not master_list:
4089 return None, ('No matching master for builder %s.' % builder)
4090 elif len(master_list) > 1:
4091 return None, ('The builder name %s exists in multiple masters %s.' %
4092 (builder, master_list))
4093 else:
4094 cur_master = master_list[0]
4095 if not result_master:
4096 result_master = cur_master
4097 elif result_master != cur_master:
4098 return None, 'The builders do not belong to the same master.'
4099 return result_master, None
4100
4101
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004102def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004103 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004104 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004105 status = GetTreeStatus()
4106 if 'unset' == status:
4107 print 'You must configure your tree status URL by running "git cl config".'
4108 return 2
4109
4110 print "The tree is %s" % status
4111 print
4112 print GetTreeStatusReason()
4113 if status != 'open':
4114 return 1
4115 return 0
4116
4117
maruel@chromium.org15192402012-09-06 12:38:29 +00004118def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004119 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004120 group = optparse.OptionGroup(parser, "Try job options")
4121 group.add_option(
4122 "-b", "--bot", action="append",
4123 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4124 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004125 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004126 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004127 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004128 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004129 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004130 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004131 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004132 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004133 "-r", "--revision",
4134 help="Revision to use for the try job; default: the "
4135 "revision will be determined by the try server; see "
4136 "its waterfall for more info")
4137 group.add_option(
4138 "-c", "--clobber", action="store_true", default=False,
4139 help="Force a clobber before building; e.g. don't do an "
4140 "incremental build")
4141 group.add_option(
4142 "--project",
4143 help="Override which project to use. Projects are defined "
4144 "server-side to define what default bot set to use")
4145 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004146 "-p", "--property", dest="properties", action="append", default=[],
4147 help="Specify generic properties in the form -p key1=value1 -p "
4148 "key2=value2 etc (buildbucket only). The value will be treated as "
4149 "json if decodable, or as string otherwise.")
4150 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004151 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004152 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004153 "--use-rietveld", action="store_true", default=False,
4154 help="Use Rietveld to trigger try jobs.")
4155 group.add_option(
4156 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4157 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004158 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004159 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004160 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004161 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004162
machenbach@chromium.org45453142015-09-15 08:45:22 +00004163 if options.use_rietveld and options.properties:
4164 parser.error('Properties can only be specified with buildbucket')
4165
4166 # Make sure that all properties are prop=value pairs.
4167 bad_params = [x for x in options.properties if '=' not in x]
4168 if bad_params:
4169 parser.error('Got properties with missing "=": %s' % bad_params)
4170
maruel@chromium.org15192402012-09-06 12:38:29 +00004171 if args:
4172 parser.error('Unknown arguments: %s' % args)
4173
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004174 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004175 if not cl.GetIssue():
4176 parser.error('Need to upload first')
4177
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004178 if cl.IsGerrit():
4179 parser.error(
4180 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4181 'If your project has Commit Queue, dry run is a workaround:\n'
4182 ' git cl set-commit --dry-run')
4183 # Code below assumes Rietveld issue.
4184 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4185
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004186 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004187 if props.get('closed'):
4188 parser.error('Cannot send tryjobs for a closed CL')
4189
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004190 if props.get('private'):
4191 parser.error('Cannot use trybots with private issue')
4192
maruel@chromium.org15192402012-09-06 12:38:29 +00004193 if not options.name:
4194 options.name = cl.GetBranch()
4195
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004196 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004197 options.master, err_msg = GetBuilderMaster(options.bot)
4198 if err_msg:
4199 parser.error('Tryserver master cannot be found because: %s\n'
4200 'Please manually specify the tryserver master'
4201 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004202
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004203 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004204 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004205 if not options.bot:
4206 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004207
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004208 # Get try masters from PRESUBMIT.py files.
4209 masters = presubmit_support.DoGetTryMasters(
4210 change,
4211 change.LocalPaths(),
4212 settings.GetRoot(),
4213 None,
4214 None,
4215 options.verbose,
4216 sys.stdout)
4217 if masters:
4218 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004219
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004220 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4221 options.bot = presubmit_support.DoGetTrySlaves(
4222 change,
4223 change.LocalPaths(),
4224 settings.GetRoot(),
4225 None,
4226 None,
4227 options.verbose,
4228 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004229
4230 if not options.bot:
4231 # Get try masters from cq.cfg if any.
4232 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4233 # location.
4234 cq_cfg = os.path.join(change.RepositoryRoot(),
4235 'infra', 'config', 'cq.cfg')
4236 if os.path.exists(cq_cfg):
4237 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004238 cq_masters = commit_queue.get_master_builder_map(
4239 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004240 for master, builders in cq_masters.iteritems():
4241 for builder in builders:
4242 # Skip presubmit builders, because these will fail without LGTM.
4243 if 'presubmit' not in builder.lower():
4244 masters.setdefault(master, {})[builder] = ['defaulttests']
4245 if masters:
4246 return masters
4247
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004248 if not options.bot:
4249 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004250
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004251 builders_and_tests = {}
4252 # TODO(machenbach): The old style command-line options don't support
4253 # multiple try masters yet.
4254 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4255 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4256
4257 for bot in old_style:
4258 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004259 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004260 elif ',' in bot:
4261 parser.error('Specify one bot per --bot flag')
4262 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004263 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004264
4265 for bot, tests in new_style:
4266 builders_and_tests.setdefault(bot, []).extend(tests)
4267
4268 # Return a master map with one master to be backwards compatible. The
4269 # master name defaults to an empty string, which will cause the master
4270 # not to be set on rietveld (deprecated).
4271 return {options.master: builders_and_tests}
4272
4273 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004274
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004275 for builders in masters.itervalues():
4276 if any('triggered' in b for b in builders):
4277 print >> sys.stderr, (
4278 'ERROR You are trying to send a job to a triggered bot. This type of'
4279 ' bot requires an\ninitial job from a parent (usually a builder). '
4280 'Instead send your job to the parent.\n'
4281 'Bot list: %s' % builders)
4282 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004283
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004284 patchset = cl.GetMostRecentPatchset()
4285 if patchset and patchset != cl.GetPatchset():
4286 print(
4287 '\nWARNING Mismatch between local config and server. Did a previous '
4288 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4289 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004290 if options.luci:
4291 trigger_luci_job(cl, masters, options)
4292 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004293 try:
4294 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4295 except BuildbucketResponseException as ex:
4296 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004297 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004298 except Exception as e:
4299 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4300 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4301 e, stacktrace)
4302 return 1
4303 else:
4304 try:
4305 cl.RpcServer().trigger_distributed_try_jobs(
4306 cl.GetIssue(), patchset, options.name, options.clobber,
4307 options.revision, masters)
4308 except urllib2.HTTPError as e:
4309 if e.code == 404:
4310 print('404 from rietveld; '
4311 'did you mean to use "git try" instead of "git cl try"?')
4312 return 1
4313 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004314
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004315 for (master, builders) in sorted(masters.iteritems()):
4316 if master:
4317 print 'Master: %s' % master
4318 length = max(len(builder) for builder in builders)
4319 for builder in sorted(builders):
4320 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004321 return 0
4322
4323
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004324def CMDtry_results(parser, args):
4325 group = optparse.OptionGroup(parser, "Try job results options")
4326 group.add_option(
4327 "-p", "--patchset", type=int, help="patchset number if not current.")
4328 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004329 "--print-master", action='store_true', help="print master name as well.")
4330 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004331 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004332 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004333 group.add_option(
4334 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4335 help="Host of buildbucket. The default host is %default.")
4336 parser.add_option_group(group)
4337 auth.add_auth_options(parser)
4338 options, args = parser.parse_args(args)
4339 if args:
4340 parser.error('Unrecognized args: %s' % ' '.join(args))
4341
4342 auth_config = auth.extract_auth_config_from_options(options)
4343 cl = Changelist(auth_config=auth_config)
4344 if not cl.GetIssue():
4345 parser.error('Need to upload first')
4346
4347 if not options.patchset:
4348 options.patchset = cl.GetMostRecentPatchset()
4349 if options.patchset and options.patchset != cl.GetPatchset():
4350 print(
4351 '\nWARNING Mismatch between local config and server. Did a previous '
4352 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4353 'Continuing using\npatchset %s.\n' % options.patchset)
4354 try:
4355 jobs = fetch_try_jobs(auth_config, cl, options)
4356 except BuildbucketResponseException as ex:
4357 print 'Buildbucket error: %s' % ex
4358 return 1
4359 except Exception as e:
4360 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4361 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4362 e, stacktrace)
4363 return 1
4364 print_tryjobs(options, jobs)
4365 return 0
4366
4367
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004368@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004370 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004371 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004372 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004373 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004374
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004375 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004376 if args:
4377 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004378 branch = cl.GetBranch()
4379 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004380 cl = Changelist()
4381 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004382
4383 # Clear configured merge-base, if there is one.
4384 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004385 else:
4386 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387 return 0
4388
4389
thestig@chromium.org00858c82013-12-02 23:08:03 +00004390def CMDweb(parser, args):
4391 """Opens the current CL in the web browser."""
4392 _, args = parser.parse_args(args)
4393 if args:
4394 parser.error('Unrecognized args: %s' % ' '.join(args))
4395
4396 issue_url = Changelist().GetIssueURL()
4397 if not issue_url:
4398 print >> sys.stderr, 'ERROR No issue to open'
4399 return 1
4400
4401 webbrowser.open(issue_url)
4402 return 0
4403
4404
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004405def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004406 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004407 parser.add_option('-d', '--dry-run', action='store_true',
4408 help='trigger in dry run mode')
4409 parser.add_option('-c', '--clear', action='store_true',
4410 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004411 auth.add_auth_options(parser)
4412 options, args = parser.parse_args(args)
4413 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004414 if args:
4415 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004416 if options.dry_run and options.clear:
4417 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4418
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004419 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004420 if options.clear:
4421 state = _CQState.CLEAR
4422 elif options.dry_run:
4423 state = _CQState.DRY_RUN
4424 else:
4425 state = _CQState.COMMIT
4426 if not cl.GetIssue():
4427 parser.error('Must upload the issue first')
4428 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004429 return 0
4430
4431
groby@chromium.org411034a2013-02-26 15:12:01 +00004432def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004433 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004434 auth.add_auth_options(parser)
4435 options, args = parser.parse_args(args)
4436 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004437 if args:
4438 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004439 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004440 # Ensure there actually is an issue to close.
4441 cl.GetDescription()
4442 cl.CloseIssue()
4443 return 0
4444
4445
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004446def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004447 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004448 auth.add_auth_options(parser)
4449 options, args = parser.parse_args(args)
4450 auth_config = auth.extract_auth_config_from_options(options)
4451 if args:
4452 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004453
4454 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004455 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004456 # Staged changes would be committed along with the patch from last
4457 # upload, hence counted toward the "last upload" side in the final
4458 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004459 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004460 return 1
4461
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004462 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004463 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004464 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004465 if not issue:
4466 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004467 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004468 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004469
4470 # Create a new branch based on the merge-base
4471 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004472 # Clear cached branch in cl object, to avoid overwriting original CL branch
4473 # properties.
4474 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004475 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004476 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004477 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004478 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004479 return rtn
4480
wychen@chromium.org06928532015-02-03 02:11:29 +00004481 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004482 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004483 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004484 finally:
4485 RunGit(['checkout', '-q', branch])
4486 RunGit(['branch', '-D', TMP_BRANCH])
4487
4488 return 0
4489
4490
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004491def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004492 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004493 parser.add_option(
4494 '--no-color',
4495 action='store_true',
4496 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004497 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004498 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004499 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004500
4501 author = RunGit(['config', 'user.email']).strip() or None
4502
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004503 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004504
4505 if args:
4506 if len(args) > 1:
4507 parser.error('Unknown args')
4508 base_branch = args[0]
4509 else:
4510 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004511 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004512
4513 change = cl.GetChange(base_branch, None)
4514 return owners_finder.OwnersFinder(
4515 [f.LocalPath() for f in
4516 cl.GetChange(base_branch, None).AffectedFiles()],
4517 change.RepositoryRoot(), author,
4518 fopen=file, os_path=os.path, glob=glob.glob,
4519 disable_color=options.no_color).run()
4520
4521
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004522def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004523 """Generates a diff command."""
4524 # Generate diff for the current branch's changes.
4525 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4526 upstream_commit, '--' ]
4527
4528 if args:
4529 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004530 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004531 diff_cmd.append(arg)
4532 else:
4533 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004534
4535 return diff_cmd
4536
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004537def MatchingFileType(file_name, extensions):
4538 """Returns true if the file name ends with one of the given extensions."""
4539 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004540
enne@chromium.org555cfe42014-01-29 18:21:39 +00004541@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004542def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004543 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004544 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004545 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004546 parser.add_option('--full', action='store_true',
4547 help='Reformat the full content of all touched files')
4548 parser.add_option('--dry-run', action='store_true',
4549 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004550 parser.add_option('--python', action='store_true',
4551 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004552 parser.add_option('--diff', action='store_true',
4553 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004554 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004555
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004556 # git diff generates paths against the root of the repository. Change
4557 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004558 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004559 if rel_base_path:
4560 os.chdir(rel_base_path)
4561
digit@chromium.org29e47272013-05-17 17:01:46 +00004562 # Grab the merge-base commit, i.e. the upstream commit of the current
4563 # branch when it was created or the last time it was rebased. This is
4564 # to cover the case where the user may have called "git fetch origin",
4565 # moving the origin branch to a newer commit, but hasn't rebased yet.
4566 upstream_commit = None
4567 cl = Changelist()
4568 upstream_branch = cl.GetUpstreamBranch()
4569 if upstream_branch:
4570 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4571 upstream_commit = upstream_commit.strip()
4572
4573 if not upstream_commit:
4574 DieWithError('Could not find base commit for this branch. '
4575 'Are you in detached state?')
4576
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004577 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4578 diff_output = RunGit(changed_files_cmd)
4579 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004580 # Filter out files deleted by this CL
4581 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004582
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004583 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4584 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4585 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004586 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004587
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004588 top_dir = os.path.normpath(
4589 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4590
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004591 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4592 # formatted. This is used to block during the presubmit.
4593 return_value = 0
4594
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004595 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004596 # Locate the clang-format binary in the checkout
4597 try:
4598 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4599 except clang_format.NotFoundError, e:
4600 DieWithError(e)
4601
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004602 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004603 cmd = [clang_format_tool]
4604 if not opts.dry_run and not opts.diff:
4605 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004606 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004607 if opts.diff:
4608 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004609 else:
4610 env = os.environ.copy()
4611 env['PATH'] = str(os.path.dirname(clang_format_tool))
4612 try:
4613 script = clang_format.FindClangFormatScriptInChromiumTree(
4614 'clang-format-diff.py')
4615 except clang_format.NotFoundError, e:
4616 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004617
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004618 cmd = [sys.executable, script, '-p0']
4619 if not opts.dry_run and not opts.diff:
4620 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004621
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004622 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4623 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004624
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004625 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4626 if opts.diff:
4627 sys.stdout.write(stdout)
4628 if opts.dry_run and len(stdout) > 0:
4629 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004630
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004631 # Similar code to above, but using yapf on .py files rather than clang-format
4632 # on C/C++ files
4633 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004634 yapf_tool = gclient_utils.FindExecutable('yapf')
4635 if yapf_tool is None:
4636 DieWithError('yapf not found in PATH')
4637
4638 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004639 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004640 cmd = [yapf_tool]
4641 if not opts.dry_run and not opts.diff:
4642 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004643 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004644 if opts.diff:
4645 sys.stdout.write(stdout)
4646 else:
4647 # TODO(sbc): yapf --lines mode still has some issues.
4648 # https://github.com/google/yapf/issues/154
4649 DieWithError('--python currently only works with --full')
4650
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004651 # Dart's formatter does not have the nice property of only operating on
4652 # modified chunks, so hard code full.
4653 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004654 try:
4655 command = [dart_format.FindDartFmtToolInChromiumTree()]
4656 if not opts.dry_run and not opts.diff:
4657 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004658 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004659
ppi@chromium.org6593d932016-03-03 15:41:15 +00004660 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004661 if opts.dry_run and stdout:
4662 return_value = 2
4663 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004664 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4665 'found in this checkout. Files in other languages are still ' +
4666 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004667
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004668 # Format GN build files. Always run on full build files for canonical form.
4669 if gn_diff_files:
4670 cmd = ['gn', 'format']
4671 if not opts.dry_run and not opts.diff:
4672 cmd.append('--in-place')
4673 for gn_diff_file in gn_diff_files:
4674 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4675 if opts.diff:
4676 sys.stdout.write(stdout)
4677
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004678 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004679
4680
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004681@subcommand.usage('<codereview url or issue id>')
4682def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004683 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004684 _, args = parser.parse_args(args)
4685
4686 if len(args) != 1:
4687 parser.print_help()
4688 return 1
4689
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004690 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004691 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004692 parser.print_help()
4693 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004694 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004695
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004696 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004697 output = RunGit(['config', '--local', '--get-regexp',
4698 r'branch\..*\.%s' % issueprefix],
4699 error_ok=True)
4700 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004701 if issue == target_issue:
4702 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004703
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004704 branches = []
4705 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004706 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004707 if len(branches) == 0:
4708 print 'No branch found for issue %s.' % target_issue
4709 return 1
4710 if len(branches) == 1:
4711 RunGit(['checkout', branches[0]])
4712 else:
4713 print 'Multiple branches match issue %s:' % target_issue
4714 for i in range(len(branches)):
4715 print '%d: %s' % (i, branches[i])
4716 which = raw_input('Choose by index: ')
4717 try:
4718 RunGit(['checkout', branches[int(which)]])
4719 except (IndexError, ValueError):
4720 print 'Invalid selection, not checking out any branch.'
4721 return 1
4722
4723 return 0
4724
4725
maruel@chromium.org29404b52014-09-08 22:58:00 +00004726def CMDlol(parser, args):
4727 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004728 print zlib.decompress(base64.b64decode(
4729 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4730 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4731 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4732 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004733 return 0
4734
4735
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004736class OptionParser(optparse.OptionParser):
4737 """Creates the option parse and add --verbose support."""
4738 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004739 optparse.OptionParser.__init__(
4740 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004741 self.add_option(
4742 '-v', '--verbose', action='count', default=0,
4743 help='Use 2 times for more debugging info')
4744
4745 def parse_args(self, args=None, values=None):
4746 options, args = optparse.OptionParser.parse_args(self, args, values)
4747 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4748 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4749 return options, args
4750
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004751
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004752def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004753 if sys.hexversion < 0x02060000:
4754 print >> sys.stderr, (
4755 '\nYour python version %s is unsupported, please upgrade.\n' %
4756 sys.version.split(' ', 1)[0])
4757 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004758
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004759 # Reload settings.
4760 global settings
4761 settings = Settings()
4762
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004763 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004764 dispatcher = subcommand.CommandDispatcher(__name__)
4765 try:
4766 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004767 except auth.AuthenticationError as e:
4768 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004769 except urllib2.HTTPError, e:
4770 if e.code != 500:
4771 raise
4772 DieWithError(
4773 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4774 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004775 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004776
4777
4778if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004779 # These affect sys.stdout so do it outside of main() to simplify mocks in
4780 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004781 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004782 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004783 try:
4784 sys.exit(main(sys.argv[1:]))
4785 except KeyboardInterrupt:
4786 sys.stderr.write('interrupted\n')
4787 sys.exit(1)