blob: fb12eafe9af6f444b916a00a09a0225b7aafe1fd [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000838class _ParsedIssueNumberArgument(object):
839 def __init__(self, issue=None, patchset=None, hostname=None):
840 self.issue = issue
841 self.patchset = patchset
842 self.hostname = hostname
843
844 @property
845 def valid(self):
846 return self.issue is not None
847
848
849class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
850 def __init__(self, *args, **kwargs):
851 self.patch_url = kwargs.pop('patch_url', None)
852 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
853
854
855def ParseIssueNumberArgument(arg):
856 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
857 fail_result = _ParsedIssueNumberArgument()
858
859 if arg.isdigit():
860 return _ParsedIssueNumberArgument(issue=int(arg))
861 if not arg.startswith('http'):
862 return fail_result
863 url = gclient_utils.UpgradeToHttps(arg)
864 try:
865 parsed_url = urlparse.urlparse(url)
866 except ValueError:
867 return fail_result
868 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
869 tmp = cls.ParseIssueURL(parsed_url)
870 if tmp is not None:
871 return tmp
872 return fail_result
873
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000876 """Changelist works with one changelist in local branch.
877
878 Supports two codereview backends: Rietveld or Gerrit, selected at object
879 creation.
880
881 Not safe for concurrent multi-{thread,process} use.
882 """
883
884 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
885 """Create a new ChangeList instance.
886
887 If issue is given, the codereview must be given too.
888
889 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
890 Otherwise, it's decided based on current configuration of the local branch,
891 with default being 'rietveld' for backwards compatibility.
892 See _load_codereview_impl for more details.
893
894 **kwargs will be passed directly to codereview implementation.
895 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000897 global settings
898 if not settings:
899 # Happens when git_cl.py is used as a utility library.
900 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901
902 if issue:
903 assert codereview, 'codereview must be known, if issue is known'
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.branchref = branchref
906 if self.branchref:
907 self.branch = ShortBranchName(self.branchref)
908 else:
909 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000911 self.lookedup_issue = False
912 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.has_description = False
914 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000915 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000917 self.cc = None
918 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000922 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000924 assert self._codereview_impl
925 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 def _load_codereview_impl(self, codereview=None, **kwargs):
928 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000929 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
930 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
931 self._codereview = codereview
932 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 return
934
935 # Automatic selection based on issue number set for a current branch.
936 # Rietveld takes precedence over Gerrit.
937 assert not self.issue
938 # Whether we find issue or not, we are doing the lookup.
939 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000940 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 setting = cls.IssueSetting(self.GetBranch())
942 issue = RunGit(['config', setting], error_ok=True).strip()
943 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = cls(self, **kwargs)
946 self.issue = int(issue)
947 return
948
949 # No issue is set for this branch, so decide based on repo-wide settings.
950 return self._load_codereview_impl(
951 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
952 **kwargs)
953
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 def IsGerrit(self):
955 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000956
957 def GetCCList(self):
958 """Return the users cc'd on this CL.
959
960 Return is a string suitable for passing to gcl with the --cc flag.
961 """
962 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000963 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000964 more_cc = ','.join(self.watchers)
965 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
966 return self.cc
967
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000968 def GetCCListWithoutDefault(self):
969 """Return the users cc'd on this CL excluding default ones."""
970 if self.cc is None:
971 self.cc = ','.join(self.watchers)
972 return self.cc
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def SetWatchers(self, watchers):
975 """Set the list of email addresses that should be cc'd based on the changed
976 files in this CL.
977 """
978 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
980 def GetBranch(self):
981 """Returns the short branch name, e.g. 'master'."""
982 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000984 if not branchref:
985 return None
986 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 self.branch = ShortBranchName(self.branchref)
988 return self.branch
989
990 def GetBranchRef(self):
991 """Returns the full branch name, e.g. 'refs/heads/master'."""
992 self.GetBranch() # Poke the lazy loader.
993 return self.branchref
994
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000995 @staticmethod
996 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000997 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 e.g. 'origin', 'refs/heads/master'
999 """
1000 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1002 error_ok=True).strip()
1003 if upstream_branch:
1004 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1005 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001006 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1007 error_ok=True).strip()
1008 if upstream_branch:
1009 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001011 # Fall back on trying a git-svn upstream branch.
1012 if settings.GetIsGitSvn():
1013 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Else, try to guess the origin remote.
1016 remote_branches = RunGit(['branch', '-r']).split()
1017 if 'origin/master' in remote_branches:
1018 # Fall back on origin/master if it exits.
1019 remote = 'origin'
1020 upstream_branch = 'refs/heads/master'
1021 elif 'origin/trunk' in remote_branches:
1022 # Fall back on origin/trunk if it exists. Generally a shared
1023 # git-svn clone
1024 remote = 'origin'
1025 upstream_branch = 'refs/heads/trunk'
1026 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027 DieWithError(
1028 'Unable to determine default branch to diff against.\n'
1029 'Either pass complete "git diff"-style arguments, like\n'
1030 ' git cl upload origin/master\n'
1031 'or verify this branch is set up to track another \n'
1032 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033
1034 return remote, upstream_branch
1035
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001036 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001037 upstream_branch = self.GetUpstreamBranch()
1038 if not BranchExists(upstream_branch):
1039 DieWithError('The upstream for the current branch (%s) does not exist '
1040 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001041 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001042 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 def GetUpstreamBranch(self):
1045 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001046 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001048 upstream_branch = upstream_branch.replace('refs/heads/',
1049 'refs/remotes/%s/' % remote)
1050 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1051 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052 self.upstream_branch = upstream_branch
1053 return self.upstream_branch
1054
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001056 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001057 remote, branch = None, self.GetBranch()
1058 seen_branches = set()
1059 while branch not in seen_branches:
1060 seen_branches.add(branch)
1061 remote, branch = self.FetchUpstreamTuple(branch)
1062 branch = ShortBranchName(branch)
1063 if remote != '.' or branch.startswith('refs/remotes'):
1064 break
1065 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001066 remotes = RunGit(['remote'], error_ok=True).split()
1067 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001068 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001069 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001070 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001071 logging.warning('Could not determine which remote this change is '
1072 'associated with, so defaulting to "%s". This may '
1073 'not be what you want. You may prevent this message '
1074 'by running "git svn info" as documented here: %s',
1075 self._remote,
1076 GIT_INSTRUCTIONS_URL)
1077 else:
1078 logging.warn('Could not determine which remote this change is '
1079 'associated with. You may prevent this message by '
1080 'running "git svn info" as documented here: %s',
1081 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001082 branch = 'HEAD'
1083 if branch.startswith('refs/remotes'):
1084 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001085 elif branch.startswith('refs/branch-heads/'):
1086 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001087 else:
1088 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001089 return self._remote
1090
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 def GitSanityChecks(self, upstream_git_obj):
1092 """Checks git repo status and ensures diff is from local commits."""
1093
sbc@chromium.org79706062015-01-14 21:18:12 +00001094 if upstream_git_obj is None:
1095 if self.GetBranch() is None:
1096 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001097 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 else:
1099 print >> sys.stderr, (
1100 'ERROR: no upstream branch')
1101 return False
1102
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 # Verify the commit we're diffing against is in our current branch.
1104 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1105 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1106 if upstream_sha != common_ancestor:
1107 print >> sys.stderr, (
1108 'ERROR: %s is not in the current branch. You may need to rebase '
1109 'your tracking branch' % upstream_sha)
1110 return False
1111
1112 # List the commits inside the diff, and verify they are all local.
1113 commits_in_diff = RunGit(
1114 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1115 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1116 remote_branch = remote_branch.strip()
1117 if code != 0:
1118 _, remote_branch = self.GetRemoteBranch()
1119
1120 commits_in_remote = RunGit(
1121 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1122
1123 common_commits = set(commits_in_diff) & set(commits_in_remote)
1124 if common_commits:
1125 print >> sys.stderr, (
1126 'ERROR: Your diff contains %d commits already in %s.\n'
1127 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1128 'the diff. If you are using a custom git flow, you can override'
1129 ' the reference used for this check with "git config '
1130 'gitcl.remotebranch <git-ref>".' % (
1131 len(common_commits), remote_branch, upstream_git_obj))
1132 return False
1133 return True
1134
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001135 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001136 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001137
1138 Returns None if it is not set.
1139 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1141 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001142
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001143 def GetGitSvnRemoteUrl(self):
1144 """Return the configured git-svn remote URL parsed from git svn info.
1145
1146 Returns None if it is not set.
1147 """
1148 # URL is dependent on the current directory.
1149 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1150 if data:
1151 keys = dict(line.split(': ', 1) for line in data.splitlines()
1152 if ': ' in line)
1153 return keys.get('URL', None)
1154 return None
1155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 def GetRemoteUrl(self):
1157 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1158
1159 Returns None if there is no remote.
1160 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001161 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001162 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1163
1164 # If URL is pointing to a local directory, it is probably a git cache.
1165 if os.path.isdir(url):
1166 url = RunGit(['config', 'remote.%s.url' % remote],
1167 error_ok=True,
1168 cwd=url).strip()
1169 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001171 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001172 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001173 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174 issue = RunGit(['config',
1175 self._codereview_impl.IssueSetting(self.GetBranch())],
1176 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001177 self.issue = int(issue) or None if issue else None
1178 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179 return self.issue
1180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 def GetIssueURL(self):
1182 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001183 issue = self.GetIssue()
1184 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001185 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001186 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
1188 def GetDescription(self, pretty=False):
1189 if not self.has_description:
1190 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001191 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 self.has_description = True
1193 if pretty:
1194 wrapper = textwrap.TextWrapper()
1195 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1196 return wrapper.fill(self.description)
1197 return self.description
1198
1199 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001200 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001201 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001204 self.patchset = int(patchset) or None if patchset else None
1205 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 return self.patchset
1207
1208 def SetPatchset(self, patchset):
1209 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001210 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001212 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001213 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001215 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001216 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001219 def SetIssue(self, issue=None):
1220 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001221 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1222 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 RunGit(['config', issue_setting, str(issue)])
1226 codereview_server = self._codereview_impl.GetCodereviewServer()
1227 if codereview_server:
1228 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001230 current_issue = self.GetIssue()
1231 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233 self.issue = None
1234 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001236 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001237 if not self.GitSanityChecks(upstream_branch):
1238 DieWithError('\nGit sanity check failure')
1239
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001240 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001241 if not root:
1242 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001243 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001244
1245 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001246 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001248 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001249 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001250 except subprocess2.CalledProcessError:
1251 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001252 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001253 'This branch probably doesn\'t exist anymore. To reset the\n'
1254 'tracking branch, please run\n'
1255 ' git branch --set-upstream %s trunk\n'
1256 'replacing trunk with origin/master or the relevant branch') %
1257 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001258
maruel@chromium.org52424302012-08-29 15:14:30 +00001259 issue = self.GetIssue()
1260 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001261 if issue:
1262 description = self.GetDescription()
1263 else:
1264 # If the change was never uploaded, use the log messages of all commits
1265 # up to the branch point, as git cl upload will prefill the description
1266 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001267 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1268 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001269
1270 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001271 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001272 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001273 name,
1274 description,
1275 absroot,
1276 files,
1277 issue,
1278 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001279 author,
1280 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001281
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001282 def UpdateDescription(self, description):
1283 self.description = description
1284 return self._codereview_impl.UpdateDescriptionRemote(description)
1285
1286 def RunHook(self, committing, may_prompt, verbose, change):
1287 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1288 try:
1289 return presubmit_support.DoPresubmitChecks(change, committing,
1290 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1291 default_presubmit=None, may_prompt=may_prompt,
1292 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1293 except presubmit_support.PresubmitFailure, e:
1294 DieWithError(
1295 ('%s\nMaybe your depot_tools is out of date?\n'
1296 'If all fails, contact maruel@') % e)
1297
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001298 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1299 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001300 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1301 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001302 else:
1303 # Assume url.
1304 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1305 urlparse.urlparse(issue_arg))
1306 if not parsed_issue_arg or not parsed_issue_arg.valid:
1307 DieWithError('Failed to parse issue argument "%s". '
1308 'Must be an issue number or a valid URL.' % issue_arg)
1309 return self._codereview_impl.CMDPatchWithParsedIssue(
1310 parsed_issue_arg, reject, nocommit, directory)
1311
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001312 # Forward methods to codereview specific implementation.
1313
1314 def CloseIssue(self):
1315 return self._codereview_impl.CloseIssue()
1316
1317 def GetStatus(self):
1318 return self._codereview_impl.GetStatus()
1319
1320 def GetCodereviewServer(self):
1321 return self._codereview_impl.GetCodereviewServer()
1322
1323 def GetApprovingReviewers(self):
1324 return self._codereview_impl.GetApprovingReviewers()
1325
1326 def GetMostRecentPatchset(self):
1327 return self._codereview_impl.GetMostRecentPatchset()
1328
1329 def __getattr__(self, attr):
1330 # This is because lots of untested code accesses Rietveld-specific stuff
1331 # directly, and it's hard to fix for sure. So, just let it work, and fix
1332 # on a cases by case basis.
1333 return getattr(self._codereview_impl, attr)
1334
1335
1336class _ChangelistCodereviewBase(object):
1337 """Abstract base class encapsulating codereview specifics of a changelist."""
1338 def __init__(self, changelist):
1339 self._changelist = changelist # instance of Changelist
1340
1341 def __getattr__(self, attr):
1342 # Forward methods to changelist.
1343 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1344 # _RietveldChangelistImpl to avoid this hack?
1345 return getattr(self._changelist, attr)
1346
1347 def GetStatus(self):
1348 """Apply a rough heuristic to give a simple summary of an issue's review
1349 or CQ status, assuming adherence to a common workflow.
1350
1351 Returns None if no issue for this branch, or specific string keywords.
1352 """
1353 raise NotImplementedError()
1354
1355 def GetCodereviewServer(self):
1356 """Returns server URL without end slash, like "https://codereview.com"."""
1357 raise NotImplementedError()
1358
1359 def FetchDescription(self):
1360 """Fetches and returns description from the codereview server."""
1361 raise NotImplementedError()
1362
1363 def GetCodereviewServerSetting(self):
1364 """Returns git config setting for the codereview server."""
1365 raise NotImplementedError()
1366
1367 @staticmethod
1368 def IssueSetting(branch):
1369 """Returns name of git config setting which stores issue number for a given
1370 branch."""
1371 raise NotImplementedError()
1372
1373 def PatchsetSetting(self):
1374 """Returns name of git config setting which stores issue number."""
1375 raise NotImplementedError()
1376
1377 def GetRieveldObjForPresubmit(self):
1378 # This is an unfortunate Rietveld-embeddedness in presubmit.
1379 # For non-Rietveld codereviews, this probably should return a dummy object.
1380 raise NotImplementedError()
1381
1382 def UpdateDescriptionRemote(self, description):
1383 """Update the description on codereview site."""
1384 raise NotImplementedError()
1385
1386 def CloseIssue(self):
1387 """Closes the issue."""
1388 raise NotImplementedError()
1389
1390 def GetApprovingReviewers(self):
1391 """Returns a list of reviewers approving the change.
1392
1393 Note: not necessarily committers.
1394 """
1395 raise NotImplementedError()
1396
1397 def GetMostRecentPatchset(self):
1398 """Returns the most recent patchset number from the codereview site."""
1399 raise NotImplementedError()
1400
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001401 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1402 directory):
1403 """Fetches and applies the issue.
1404
1405 Arguments:
1406 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1407 reject: if True, reject the failed patch instead of switching to 3-way
1408 merge. Rietveld only.
1409 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1410 only.
1411 directory: switch to directory before applying the patch. Rietveld only.
1412 """
1413 raise NotImplementedError()
1414
1415 @staticmethod
1416 def ParseIssueURL(parsed_url):
1417 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1418 failed."""
1419 raise NotImplementedError()
1420
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421
1422class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1423 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1424 super(_RietveldChangelistImpl, self).__init__(changelist)
1425 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1426 settings.GetDefaultServerUrl()
1427
1428 self._rietveld_server = rietveld_server
1429 self._auth_config = auth_config
1430 self._props = None
1431 self._rpc_server = None
1432
1433 def GetAuthConfig(self):
1434 return self._auth_config
1435
1436 def GetCodereviewServer(self):
1437 if not self._rietveld_server:
1438 # If we're on a branch then get the server potentially associated
1439 # with that branch.
1440 if self.GetIssue():
1441 rietveld_server_setting = self.GetCodereviewServerSetting()
1442 if rietveld_server_setting:
1443 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1444 ['config', rietveld_server_setting], error_ok=True).strip())
1445 if not self._rietveld_server:
1446 self._rietveld_server = settings.GetDefaultServerUrl()
1447 return self._rietveld_server
1448
1449 def FetchDescription(self):
1450 issue = self.GetIssue()
1451 assert issue
1452 try:
1453 return self.RpcServer().get_description(issue).strip()
1454 except urllib2.HTTPError as e:
1455 if e.code == 404:
1456 DieWithError(
1457 ('\nWhile fetching the description for issue %d, received a '
1458 '404 (not found)\n'
1459 'error. It is likely that you deleted this '
1460 'issue on the server. If this is the\n'
1461 'case, please run\n\n'
1462 ' git cl issue 0\n\n'
1463 'to clear the association with the deleted issue. Then run '
1464 'this command again.') % issue)
1465 else:
1466 DieWithError(
1467 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1468 except urllib2.URLError as e:
1469 print >> sys.stderr, (
1470 'Warning: Failed to retrieve CL description due to network '
1471 'failure.')
1472 return ''
1473
1474 def GetMostRecentPatchset(self):
1475 return self.GetIssueProperties()['patchsets'][-1]
1476
1477 def GetPatchSetDiff(self, issue, patchset):
1478 return self.RpcServer().get(
1479 '/download/issue%s_%s.diff' % (issue, patchset))
1480
1481 def GetIssueProperties(self):
1482 if self._props is None:
1483 issue = self.GetIssue()
1484 if not issue:
1485 self._props = {}
1486 else:
1487 self._props = self.RpcServer().get_issue_properties(issue, True)
1488 return self._props
1489
1490 def GetApprovingReviewers(self):
1491 return get_approving_reviewers(self.GetIssueProperties())
1492
1493 def AddComment(self, message):
1494 return self.RpcServer().add_comment(self.GetIssue(), message)
1495
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001496 def GetStatus(self):
1497 """Apply a rough heuristic to give a simple summary of an issue's review
1498 or CQ status, assuming adherence to a common workflow.
1499
1500 Returns None if no issue for this branch, or one of the following keywords:
1501 * 'error' - error from review tool (including deleted issues)
1502 * 'unsent' - not sent for review
1503 * 'waiting' - waiting for review
1504 * 'reply' - waiting for owner to reply to review
1505 * 'lgtm' - LGTM from at least one approved reviewer
1506 * 'commit' - in the commit queue
1507 * 'closed' - closed
1508 """
1509 if not self.GetIssue():
1510 return None
1511
1512 try:
1513 props = self.GetIssueProperties()
1514 except urllib2.HTTPError:
1515 return 'error'
1516
1517 if props.get('closed'):
1518 # Issue is closed.
1519 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001520 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001521 # Issue is in the commit queue.
1522 return 'commit'
1523
1524 try:
1525 reviewers = self.GetApprovingReviewers()
1526 except urllib2.HTTPError:
1527 return 'error'
1528
1529 if reviewers:
1530 # Was LGTM'ed.
1531 return 'lgtm'
1532
1533 messages = props.get('messages') or []
1534
1535 if not messages:
1536 # No message was sent.
1537 return 'unsent'
1538 if messages[-1]['sender'] != props.get('owner_email'):
1539 # Non-LGTM reply from non-owner
1540 return 'reply'
1541 return 'waiting'
1542
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001543 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001544 return self.RpcServer().update_description(
1545 self.GetIssue(), self.description)
1546
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001548 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001549
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001550 def SetFlag(self, flag, value):
1551 """Patchset must match."""
1552 if not self.GetPatchset():
1553 DieWithError('The patchset needs to match. Send another patchset.')
1554 try:
1555 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001556 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001557 except urllib2.HTTPError, e:
1558 if e.code == 404:
1559 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1560 if e.code == 403:
1561 DieWithError(
1562 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1563 'match?') % (self.GetIssue(), self.GetPatchset()))
1564 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001566 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567 """Returns an upload.RpcServer() to access this review's rietveld instance.
1568 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001569 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001570 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001571 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001572 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001573 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001574
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575 @staticmethod
1576 def IssueSetting(branch):
1577 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 """Return the git setting that stores this change's most recent patchset."""
1581 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001585 branch = self.GetBranch()
1586 if branch:
1587 return 'branch.%s.rietveldserver' % branch
1588 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590 def GetRieveldObjForPresubmit(self):
1591 return self.RpcServer()
1592
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001593 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1594 directory):
1595 # TODO(maruel): Use apply_issue.py
1596
1597 # PatchIssue should never be called with a dirty tree. It is up to the
1598 # caller to check this, but just in case we assert here since the
1599 # consequences of the caller not checking this could be dire.
1600 assert(not git_common.is_dirty_git_tree('apply'))
1601 assert(parsed_issue_arg.valid)
1602 self._changelist.issue = parsed_issue_arg.issue
1603 if parsed_issue_arg.hostname:
1604 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1605
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001606 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1607 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001608 assert parsed_issue_arg.patchset
1609 patchset = parsed_issue_arg.patchset
1610 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1611 else:
1612 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1613 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1614
1615 # Switch up to the top-level directory, if necessary, in preparation for
1616 # applying the patch.
1617 top = settings.GetRelativeRoot()
1618 if top:
1619 os.chdir(top)
1620
1621 # Git patches have a/ at the beginning of source paths. We strip that out
1622 # with a sed script rather than the -p flag to patch so we can feed either
1623 # Git or svn-style patches into the same apply command.
1624 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1625 try:
1626 patch_data = subprocess2.check_output(
1627 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1628 except subprocess2.CalledProcessError:
1629 DieWithError('Git patch mungling failed.')
1630 logging.info(patch_data)
1631
1632 # We use "git apply" to apply the patch instead of "patch" so that we can
1633 # pick up file adds.
1634 # The --index flag means: also insert into the index (so we catch adds).
1635 cmd = ['git', 'apply', '--index', '-p0']
1636 if directory:
1637 cmd.extend(('--directory', directory))
1638 if reject:
1639 cmd.append('--reject')
1640 elif IsGitVersionAtLeast('1.7.12'):
1641 cmd.append('--3way')
1642 try:
1643 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1644 stdin=patch_data, stdout=subprocess2.VOID)
1645 except subprocess2.CalledProcessError:
1646 print 'Failed to apply the patch'
1647 return 1
1648
1649 # If we had an issue, commit the current state and register the issue.
1650 if not nocommit:
1651 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1652 'patch from issue %(i)s at patchset '
1653 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1654 % {'i': self.GetIssue(), 'p': patchset})])
1655 self.SetIssue(self.GetIssue())
1656 self.SetPatchset(patchset)
1657 print "Committed patch locally."
1658 else:
1659 print "Patch applied to index."
1660 return 0
1661
1662 @staticmethod
1663 def ParseIssueURL(parsed_url):
1664 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1665 return None
1666 # Typical url: https://domain/<issue_number>[/[other]]
1667 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1668 if match:
1669 return _RietveldParsedIssueNumberArgument(
1670 issue=int(match.group(1)),
1671 hostname=parsed_url.netloc)
1672 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1673 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1674 if match:
1675 return _RietveldParsedIssueNumberArgument(
1676 issue=int(match.group(1)),
1677 patchset=int(match.group(2)),
1678 hostname=parsed_url.netloc,
1679 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1680 return None
1681
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682
1683class _GerritChangelistImpl(_ChangelistCodereviewBase):
1684 def __init__(self, changelist, auth_config=None):
1685 # auth_config is Rietveld thing, kept here to preserve interface only.
1686 super(_GerritChangelistImpl, self).__init__(changelist)
1687 self._change_id = None
1688 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1689 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1690
1691 def _GetGerritHost(self):
1692 # Lazy load of configs.
1693 self.GetCodereviewServer()
1694 return self._gerrit_host
1695
1696 def GetCodereviewServer(self):
1697 if not self._gerrit_server:
1698 # If we're on a branch then get the server potentially associated
1699 # with that branch.
1700 if self.GetIssue():
1701 gerrit_server_setting = self.GetCodereviewServerSetting()
1702 if gerrit_server_setting:
1703 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1704 error_ok=True).strip()
1705 if self._gerrit_server:
1706 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1707 if not self._gerrit_server:
1708 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1709 # has "-review" suffix for lowest level subdomain.
1710 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1711 parts[0] = parts[0] + '-review'
1712 self._gerrit_host = '.'.join(parts)
1713 self._gerrit_server = 'https://%s' % self._gerrit_host
1714 return self._gerrit_server
1715
1716 @staticmethod
1717 def IssueSetting(branch):
1718 return 'branch.%s.gerritissue' % branch
1719
1720 def PatchsetSetting(self):
1721 """Return the git setting that stores this change's most recent patchset."""
1722 return 'branch.%s.gerritpatchset' % self.GetBranch()
1723
1724 def GetCodereviewServerSetting(self):
1725 """Returns the git setting that stores this change's Gerrit server."""
1726 branch = self.GetBranch()
1727 if branch:
1728 return 'branch.%s.gerritserver' % branch
1729 return None
1730
1731 def GetRieveldObjForPresubmit(self):
1732 class ThisIsNotRietveldIssue(object):
1733 def __nonzero__(self):
1734 # This is a hack to make presubmit_support think that rietveld is not
1735 # defined, yet still ensure that calls directly result in a decent
1736 # exception message below.
1737 return False
1738
1739 def __getattr__(self, attr):
1740 print(
1741 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1742 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1743 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1744 'or use Rietveld for codereview.\n'
1745 'See also http://crbug.com/579160.' % attr)
1746 raise NotImplementedError()
1747 return ThisIsNotRietveldIssue()
1748
1749 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001750 """Apply a rough heuristic to give a simple summary of an issue's review
1751 or CQ status, assuming adherence to a common workflow.
1752
1753 Returns None if no issue for this branch, or one of the following keywords:
1754 * 'error' - error from review tool (including deleted issues)
1755 * 'unsent' - no reviewers added
1756 * 'waiting' - waiting for review
1757 * 'reply' - waiting for owner to reply to review
1758 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1759 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1760 * 'commit' - in the commit queue
1761 * 'closed' - abandoned
1762 """
1763 if not self.GetIssue():
1764 return None
1765
1766 try:
1767 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1768 except httplib.HTTPException:
1769 return 'error'
1770
1771 if data['status'] == 'ABANDONED':
1772 return 'closed'
1773
1774 cq_label = data['labels'].get('Commit-Queue', {})
1775 if cq_label:
1776 # Vote value is a stringified integer, which we expect from 0 to 2.
1777 vote_value = cq_label.get('value', '0')
1778 vote_text = cq_label.get('values', {}).get(vote_value, '')
1779 if vote_text.lower() == 'commit':
1780 return 'commit'
1781
1782 lgtm_label = data['labels'].get('Code-Review', {})
1783 if lgtm_label:
1784 if 'rejected' in lgtm_label:
1785 return 'not lgtm'
1786 if 'approved' in lgtm_label:
1787 return 'lgtm'
1788
1789 if not data.get('reviewers', {}).get('REVIEWER', []):
1790 return 'unsent'
1791
1792 messages = data.get('messages', [])
1793 if messages:
1794 owner = data['owner'].get('_account_id')
1795 last_message_author = messages[-1].get('author', {}).get('_account_id')
1796 if owner != last_message_author:
1797 # Some reply from non-owner.
1798 return 'reply'
1799
1800 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801
1802 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001803 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001804 return data['revisions'][data['current_revision']]['_number']
1805
1806 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001807 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 return data['revisions'][data['current_revision']]['commit_with_footers']
1809
1810 def UpdateDescriptionRemote(self, description):
1811 # TODO(tandrii)
1812 raise NotImplementedError()
1813
1814 def CloseIssue(self):
1815 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1816
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001817 def SubmitIssue(self, wait_for_merge=True):
1818 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
1819 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001821 def _GetChangeDetail(self, options):
1822 return gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1823 options)
1824
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001825 def CMDLand(self, force, bypass_hooks, verbose):
1826 if git_common.is_dirty_git_tree('land'):
1827 return 1
1828 differs = True
1829 last_upload = RunGit(['config',
1830 'branch.%s.gerritsquashhash' % self.GetBranch()],
1831 error_ok=True).strip()
1832 # Note: git diff outputs nothing if there is no diff.
1833 if not last_upload or RunGit(['diff', last_upload]).strip():
1834 print('WARNING: some changes from local branch haven\'t been uploaded')
1835 else:
1836 detail = self._GetChangeDetail(['CURRENT_REVISION'])
1837 if detail['current_revision'] == last_upload:
1838 differs = False
1839 else:
1840 print('WARNING: local branch contents differ from latest uploaded '
1841 'patchset')
1842 if differs:
1843 if not force:
1844 ask_for_data(
1845 'Do you want to submit latest Gerrit patchset and bypass hooks?')
1846 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
1847 elif not bypass_hooks:
1848 hook_results = self.RunHook(
1849 committing=True,
1850 may_prompt=not force,
1851 verbose=verbose,
1852 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
1853 if not hook_results.should_continue():
1854 return 1
1855
1856 self.SubmitIssue(wait_for_merge=True)
1857 print('Issue %s has been submitted.' % self.GetIssueURL())
1858 return 0
1859
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001860 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1861 directory):
1862 assert not reject
1863 assert not nocommit
1864 assert not directory
1865 assert parsed_issue_arg.valid
1866
1867 self._changelist.issue = parsed_issue_arg.issue
1868
1869 if parsed_issue_arg.hostname:
1870 self._gerrit_host = parsed_issue_arg.hostname
1871 self._gerrit_server = 'https://%s' % self._gerrit_host
1872
1873 detail = self._GetChangeDetail(['ALL_REVISIONS'])
1874
1875 if not parsed_issue_arg.patchset:
1876 # Use current revision by default.
1877 revision_info = detail['revisions'][detail['current_revision']]
1878 patchset = int(revision_info['_number'])
1879 else:
1880 patchset = parsed_issue_arg.patchset
1881 for revision_info in detail['revisions'].itervalues():
1882 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1883 break
1884 else:
1885 DieWithError('Couldn\'t find patchset %i in issue %i' %
1886 (parsed_issue_arg.patchset, self.GetIssue()))
1887
1888 fetch_info = revision_info['fetch']['http']
1889 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
1890 RunGit(['cherry-pick', 'FETCH_HEAD'])
1891 self.SetIssue(self.GetIssue())
1892 self.SetPatchset(patchset)
1893 print('Committed patch for issue %i pathset %i locally' %
1894 (self.GetIssue(), self.GetPatchset()))
1895 return 0
1896
1897 @staticmethod
1898 def ParseIssueURL(parsed_url):
1899 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1900 return None
1901 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
1902 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
1903 # Short urls like https://domain/<issue_number> can be used, but don't allow
1904 # specifying the patchset (you'd 404), but we allow that here.
1905 if parsed_url.path == '/':
1906 part = parsed_url.fragment
1907 else:
1908 part = parsed_url.path
1909 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
1910 if match:
1911 return _ParsedIssueNumberArgument(
1912 issue=int(match.group(2)),
1913 patchset=int(match.group(4)) if match.group(4) else None,
1914 hostname=parsed_url.netloc)
1915 return None
1916
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001917
1918_CODEREVIEW_IMPLEMENTATIONS = {
1919 'rietveld': _RietveldChangelistImpl,
1920 'gerrit': _GerritChangelistImpl,
1921}
1922
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001923
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001924class ChangeDescription(object):
1925 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001926 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001927 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001928
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001929 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001930 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001931
agable@chromium.org42c20792013-09-12 17:34:49 +00001932 @property # www.logilab.org/ticket/89786
1933 def description(self): # pylint: disable=E0202
1934 return '\n'.join(self._description_lines)
1935
1936 def set_description(self, desc):
1937 if isinstance(desc, basestring):
1938 lines = desc.splitlines()
1939 else:
1940 lines = [line.rstrip() for line in desc]
1941 while lines and not lines[0]:
1942 lines.pop(0)
1943 while lines and not lines[-1]:
1944 lines.pop(-1)
1945 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001946
piman@chromium.org336f9122014-09-04 02:16:55 +00001947 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001948 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001949 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001950 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001951 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001952 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001953
agable@chromium.org42c20792013-09-12 17:34:49 +00001954 # Get the set of R= and TBR= lines and remove them from the desciption.
1955 regexp = re.compile(self.R_LINE)
1956 matches = [regexp.match(line) for line in self._description_lines]
1957 new_desc = [l for i, l in enumerate(self._description_lines)
1958 if not matches[i]]
1959 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001960
agable@chromium.org42c20792013-09-12 17:34:49 +00001961 # Construct new unified R= and TBR= lines.
1962 r_names = []
1963 tbr_names = []
1964 for match in matches:
1965 if not match:
1966 continue
1967 people = cleanup_list([match.group(2).strip()])
1968 if match.group(1) == 'TBR':
1969 tbr_names.extend(people)
1970 else:
1971 r_names.extend(people)
1972 for name in r_names:
1973 if name not in reviewers:
1974 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001975 if add_owners_tbr:
1976 owners_db = owners.Database(change.RepositoryRoot(),
1977 fopen=file, os_path=os.path, glob=glob.glob)
1978 all_reviewers = set(tbr_names + reviewers)
1979 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1980 all_reviewers)
1981 tbr_names.extend(owners_db.reviewers_for(missing_files,
1982 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001983 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1984 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1985
1986 # Put the new lines in the description where the old first R= line was.
1987 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1988 if 0 <= line_loc < len(self._description_lines):
1989 if new_tbr_line:
1990 self._description_lines.insert(line_loc, new_tbr_line)
1991 if new_r_line:
1992 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001993 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001994 if new_r_line:
1995 self.append_footer(new_r_line)
1996 if new_tbr_line:
1997 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001998
1999 def prompt(self):
2000 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002001 self.set_description([
2002 '# Enter a description of the change.',
2003 '# This will be displayed on the codereview site.',
2004 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002005 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002006 '--------------------',
2007 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002008
agable@chromium.org42c20792013-09-12 17:34:49 +00002009 regexp = re.compile(self.BUG_LINE)
2010 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002011 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002012 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002013 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002014 if not content:
2015 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002016 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002017
2018 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002019 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2020 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002021 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002022 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002023
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002024 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002025 if self._description_lines:
2026 # Add an empty line if either the last line or the new line isn't a tag.
2027 last_line = self._description_lines[-1]
2028 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2029 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2030 self._description_lines.append('')
2031 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002032
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002033 def get_reviewers(self):
2034 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002035 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2036 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002037 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002038
2039
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002040def get_approving_reviewers(props):
2041 """Retrieves the reviewers that approved a CL from the issue properties with
2042 messages.
2043
2044 Note that the list may contain reviewers that are not committer, thus are not
2045 considered by the CQ.
2046 """
2047 return sorted(
2048 set(
2049 message['sender']
2050 for message in props['messages']
2051 if message['approval'] and message['sender'] in props['reviewers']
2052 )
2053 )
2054
2055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056def FindCodereviewSettingsFile(filename='codereview.settings'):
2057 """Finds the given file starting in the cwd and going up.
2058
2059 Only looks up to the top of the repository unless an
2060 'inherit-review-settings-ok' file exists in the root of the repository.
2061 """
2062 inherit_ok_file = 'inherit-review-settings-ok'
2063 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002064 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2066 root = '/'
2067 while True:
2068 if filename in os.listdir(cwd):
2069 if os.path.isfile(os.path.join(cwd, filename)):
2070 return open(os.path.join(cwd, filename))
2071 if cwd == root:
2072 break
2073 cwd = os.path.dirname(cwd)
2074
2075
2076def LoadCodereviewSettingsFromFile(fileobj):
2077 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002078 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002080 def SetProperty(name, setting, unset_error_ok=False):
2081 fullname = 'rietveld.' + name
2082 if setting in keyvals:
2083 RunGit(['config', fullname, keyvals[setting]])
2084 else:
2085 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2086
2087 SetProperty('server', 'CODE_REVIEW_SERVER')
2088 # Only server setting is required. Other settings can be absent.
2089 # In that case, we ignore errors raised during option deletion attempt.
2090 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002091 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002092 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2093 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002094 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002095 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002096 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2097 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002098 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002099 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002100 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002101 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2102 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002103
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002104 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002105 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002106
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002107 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2108 RunGit(['config', 'gerrit.squash-uploads',
2109 keyvals['GERRIT_SQUASH_UPLOADS']])
2110
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2112 #should be of the form
2113 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2114 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2115 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2116 keyvals['ORIGIN_URL_CONFIG']])
2117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002119def urlretrieve(source, destination):
2120 """urllib is broken for SSL connections via a proxy therefore we
2121 can't use urllib.urlretrieve()."""
2122 with open(destination, 'w') as f:
2123 f.write(urllib2.urlopen(source).read())
2124
2125
ukai@chromium.org712d6102013-11-27 00:52:58 +00002126def hasSheBang(fname):
2127 """Checks fname is a #! script."""
2128 with open(fname) as f:
2129 return f.read(2).startswith('#!')
2130
2131
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002132# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2133def DownloadHooks(*args, **kwargs):
2134 pass
2135
2136
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002137def DownloadGerritHook(force):
2138 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002139
2140 Args:
2141 force: True to update hooks. False to install hooks if not present.
2142 """
2143 if not settings.GetIsGerrit():
2144 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002145 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002146 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2147 if not os.access(dst, os.X_OK):
2148 if os.path.exists(dst):
2149 if not force:
2150 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002151 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002152 print(
2153 'WARNING: installing Gerrit commit-msg hook.\n'
2154 ' This behavior of git cl will soon be disabled.\n'
2155 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002156 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002157 if not hasSheBang(dst):
2158 DieWithError('Not a script: %s\n'
2159 'You need to download from\n%s\n'
2160 'into .git/hooks/commit-msg and '
2161 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002162 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2163 except Exception:
2164 if os.path.exists(dst):
2165 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002166 DieWithError('\nFailed to download hooks.\n'
2167 'You need to download from\n%s\n'
2168 'into .git/hooks/commit-msg and '
2169 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002170
2171
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002172
2173def GetRietveldCodereviewSettingsInteractively():
2174 """Prompt the user for settings."""
2175 server = settings.GetDefaultServerUrl(error_ok=True)
2176 prompt = 'Rietveld server (host[:port])'
2177 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2178 newserver = ask_for_data(prompt + ':')
2179 if not server and not newserver:
2180 newserver = DEFAULT_SERVER
2181 if newserver:
2182 newserver = gclient_utils.UpgradeToHttps(newserver)
2183 if newserver != server:
2184 RunGit(['config', 'rietveld.server', newserver])
2185
2186 def SetProperty(initial, caption, name, is_url):
2187 prompt = caption
2188 if initial:
2189 prompt += ' ("x" to clear) [%s]' % initial
2190 new_val = ask_for_data(prompt + ':')
2191 if new_val == 'x':
2192 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2193 elif new_val:
2194 if is_url:
2195 new_val = gclient_utils.UpgradeToHttps(new_val)
2196 if new_val != initial:
2197 RunGit(['config', 'rietveld.' + name, new_val])
2198
2199 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2200 SetProperty(settings.GetDefaultPrivateFlag(),
2201 'Private flag (rietveld only)', 'private', False)
2202 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2203 'tree-status-url', False)
2204 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2205 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2206 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2207 'run-post-upload-hook', False)
2208
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002209@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002210def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002211 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002212
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002213 print('WARNING: git cl config works for Rietveld only.\n'
2214 'For Gerrit, see http://crbug.com/579160.')
2215 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002216 parser.add_option('--activate-update', action='store_true',
2217 help='activate auto-updating [rietveld] section in '
2218 '.git/config')
2219 parser.add_option('--deactivate-update', action='store_true',
2220 help='deactivate auto-updating [rietveld] section in '
2221 '.git/config')
2222 options, args = parser.parse_args(args)
2223
2224 if options.deactivate_update:
2225 RunGit(['config', 'rietveld.autoupdate', 'false'])
2226 return
2227
2228 if options.activate_update:
2229 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2230 return
2231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002232 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002233 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002234 return 0
2235
2236 url = args[0]
2237 if not url.endswith('codereview.settings'):
2238 url = os.path.join(url, 'codereview.settings')
2239
2240 # Load code review settings and download hooks (if available).
2241 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2242 return 0
2243
2244
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002245def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002246 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002247 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2248 branch = ShortBranchName(branchref)
2249 _, args = parser.parse_args(args)
2250 if not args:
2251 print("Current base-url:")
2252 return RunGit(['config', 'branch.%s.base-url' % branch],
2253 error_ok=False).strip()
2254 else:
2255 print("Setting base-url to %s" % args[0])
2256 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2257 error_ok=False).strip()
2258
2259
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002260def color_for_status(status):
2261 """Maps a Changelist status to color, for CMDstatus and other tools."""
2262 return {
2263 'unsent': Fore.RED,
2264 'waiting': Fore.BLUE,
2265 'reply': Fore.YELLOW,
2266 'lgtm': Fore.GREEN,
2267 'commit': Fore.MAGENTA,
2268 'closed': Fore.CYAN,
2269 'error': Fore.WHITE,
2270 }.get(status, Fore.WHITE)
2271
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002272def fetch_cl_status(branch, auth_config=None):
2273 """Fetches information for an issue and returns (branch, issue, status)."""
2274 cl = Changelist(branchref=branch, auth_config=auth_config)
2275 url = cl.GetIssueURL()
2276 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002277
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002278 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002279 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002280 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002281
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002282 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002283
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002284def get_cl_statuses(
2285 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002286 """Returns a blocking iterable of (branch, issue, color) for given branches.
2287
2288 If fine_grained is true, this will fetch CL statuses from the server.
2289 Otherwise, simply indicate if there's a matching url for the given branches.
2290
2291 If max_processes is specified, it is used as the maximum number of processes
2292 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2293 spawned.
2294 """
2295 # Silence upload.py otherwise it becomes unwieldly.
2296 upload.verbosity = 0
2297
2298 if fine_grained:
2299 # Process one branch synchronously to work through authentication, then
2300 # spawn processes to process all the other branches in parallel.
2301 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002302 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2303 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002304
2305 branches_to_fetch = branches[1:]
2306 pool = ThreadPool(
2307 min(max_processes, len(branches_to_fetch))
2308 if max_processes is not None
2309 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002310 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002311 yield x
2312 else:
2313 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2314 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002315 cl = Changelist(branchref=b, auth_config=auth_config)
2316 url = cl.GetIssueURL()
2317 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002318
rmistry@google.com2dd99862015-06-22 12:22:18 +00002319
2320def upload_branch_deps(cl, args):
2321 """Uploads CLs of local branches that are dependents of the current branch.
2322
2323 If the local branch dependency tree looks like:
2324 test1 -> test2.1 -> test3.1
2325 -> test3.2
2326 -> test2.2 -> test3.3
2327
2328 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2329 run on the dependent branches in this order:
2330 test2.1, test3.1, test3.2, test2.2, test3.3
2331
2332 Note: This function does not rebase your local dependent branches. Use it when
2333 you make a change to the parent branch that will not conflict with its
2334 dependent branches, and you would like their dependencies updated in
2335 Rietveld.
2336 """
2337 if git_common.is_dirty_git_tree('upload-branch-deps'):
2338 return 1
2339
2340 root_branch = cl.GetBranch()
2341 if root_branch is None:
2342 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2343 'Get on a branch!')
2344 if not cl.GetIssue() or not cl.GetPatchset():
2345 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2346 'patchset dependencies without an uploaded CL.')
2347
2348 branches = RunGit(['for-each-ref',
2349 '--format=%(refname:short) %(upstream:short)',
2350 'refs/heads'])
2351 if not branches:
2352 print('No local branches found.')
2353 return 0
2354
2355 # Create a dictionary of all local branches to the branches that are dependent
2356 # on it.
2357 tracked_to_dependents = collections.defaultdict(list)
2358 for b in branches.splitlines():
2359 tokens = b.split()
2360 if len(tokens) == 2:
2361 branch_name, tracked = tokens
2362 tracked_to_dependents[tracked].append(branch_name)
2363
2364 print
2365 print 'The dependent local branches of %s are:' % root_branch
2366 dependents = []
2367 def traverse_dependents_preorder(branch, padding=''):
2368 dependents_to_process = tracked_to_dependents.get(branch, [])
2369 padding += ' '
2370 for dependent in dependents_to_process:
2371 print '%s%s' % (padding, dependent)
2372 dependents.append(dependent)
2373 traverse_dependents_preorder(dependent, padding)
2374 traverse_dependents_preorder(root_branch)
2375 print
2376
2377 if not dependents:
2378 print 'There are no dependent local branches for %s' % root_branch
2379 return 0
2380
2381 print ('This command will checkout all dependent branches and run '
2382 '"git cl upload".')
2383 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2384
andybons@chromium.org962f9462016-02-03 20:00:42 +00002385 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002386 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002387 args.extend(['-t', 'Updated patchset dependency'])
2388
rmistry@google.com2dd99862015-06-22 12:22:18 +00002389 # Record all dependents that failed to upload.
2390 failures = {}
2391 # Go through all dependents, checkout the branch and upload.
2392 try:
2393 for dependent_branch in dependents:
2394 print
2395 print '--------------------------------------'
2396 print 'Running "git cl upload" from %s:' % dependent_branch
2397 RunGit(['checkout', '-q', dependent_branch])
2398 print
2399 try:
2400 if CMDupload(OptionParser(), args) != 0:
2401 print 'Upload failed for %s!' % dependent_branch
2402 failures[dependent_branch] = 1
2403 except: # pylint: disable=W0702
2404 failures[dependent_branch] = 1
2405 print
2406 finally:
2407 # Swap back to the original root branch.
2408 RunGit(['checkout', '-q', root_branch])
2409
2410 print
2411 print 'Upload complete for dependent branches!'
2412 for dependent_branch in dependents:
2413 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2414 print ' %s : %s' % (dependent_branch, upload_status)
2415 print
2416
2417 return 0
2418
2419
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002420def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002421 """Show status of changelists.
2422
2423 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002424 - Red not sent for review or broken
2425 - Blue waiting for review
2426 - Yellow waiting for you to reply to review
2427 - Green LGTM'ed
2428 - Magenta in the commit queue
2429 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002430
2431 Also see 'git cl comments'.
2432 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002433 parser.add_option('--field',
2434 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002435 parser.add_option('-f', '--fast', action='store_true',
2436 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002437 parser.add_option(
2438 '-j', '--maxjobs', action='store', type=int,
2439 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002440
2441 auth.add_auth_options(parser)
2442 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002443 if args:
2444 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002445 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002447 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002448 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002449 if options.field.startswith('desc'):
2450 print cl.GetDescription()
2451 elif options.field == 'id':
2452 issueid = cl.GetIssue()
2453 if issueid:
2454 print issueid
2455 elif options.field == 'patch':
2456 patchset = cl.GetPatchset()
2457 if patchset:
2458 print patchset
2459 elif options.field == 'url':
2460 url = cl.GetIssueURL()
2461 if url:
2462 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002463 return 0
2464
2465 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2466 if not branches:
2467 print('No local branch found.')
2468 return 0
2469
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002470 changes = (
2471 Changelist(branchref=b, auth_config=auth_config)
2472 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002473 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002474 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002475 alignment = max(5, max(len(b) for b in branches))
2476 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002477 output = get_cl_statuses(branches,
2478 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002479 max_processes=options.maxjobs,
2480 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002481
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002482 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002483 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002484 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002485 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002486 b, i, status = output.next()
2487 branch_statuses[b] = (i, status)
2488 issue_url, status = branch_statuses.pop(branch)
2489 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002490 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00002491 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00002492 color = ''
2493 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002494 status_str = '(%s)' % status if status else ''
2495 print ' %*s : %s%s %s%s' % (
2496 alignment, ShortBranchName(branch), color, issue_url, status_str,
2497 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002498
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002499 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002500 print
2501 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002502 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002503 if not cl.GetIssue():
2504 print 'No issue assigned.'
2505 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002506 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002507 if not options.fast:
2508 print 'Issue description:'
2509 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002510 return 0
2511
2512
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002513def colorize_CMDstatus_doc():
2514 """To be called once in main() to add colors to git cl status help."""
2515 colors = [i for i in dir(Fore) if i[0].isupper()]
2516
2517 def colorize_line(line):
2518 for color in colors:
2519 if color in line.upper():
2520 # Extract whitespaces first and the leading '-'.
2521 indent = len(line) - len(line.lstrip(' ')) + 1
2522 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2523 return line
2524
2525 lines = CMDstatus.__doc__.splitlines()
2526 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2527
2528
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002529@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002530def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002531 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002532
2533 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002534 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002535 parser.add_option('-r', '--reverse', action='store_true',
2536 help='Lookup the branch(es) for the specified issues. If '
2537 'no issues are specified, all branches with mapped '
2538 'issues will be listed.')
2539 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002540
dnj@chromium.org406c4402015-03-03 17:22:28 +00002541 if options.reverse:
2542 branches = RunGit(['for-each-ref', 'refs/heads',
2543 '--format=%(refname:short)']).splitlines()
2544
2545 # Reverse issue lookup.
2546 issue_branch_map = {}
2547 for branch in branches:
2548 cl = Changelist(branchref=branch)
2549 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2550 if not args:
2551 args = sorted(issue_branch_map.iterkeys())
2552 for issue in args:
2553 if not issue:
2554 continue
2555 print 'Branch for issue number %s: %s' % (
2556 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2557 else:
2558 cl = Changelist()
2559 if len(args) > 0:
2560 try:
2561 issue = int(args[0])
2562 except ValueError:
2563 DieWithError('Pass a number to set the issue or none to list it.\n'
2564 'Maybe you want to run git cl status?')
2565 cl.SetIssue(issue)
2566 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002567 return 0
2568
2569
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002570def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002571 """Shows or posts review comments for any changelist."""
2572 parser.add_option('-a', '--add-comment', dest='comment',
2573 help='comment to add to an issue')
2574 parser.add_option('-i', dest='issue',
2575 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002576 parser.add_option('-j', '--json-file',
2577 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002578 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002579 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002580 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002581
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002582 issue = None
2583 if options.issue:
2584 try:
2585 issue = int(options.issue)
2586 except ValueError:
2587 DieWithError('A review issue id is expected to be a number')
2588
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002589 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002590
2591 if options.comment:
2592 cl.AddComment(options.comment)
2593 return 0
2594
2595 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002596 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002597 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002598 summary.append({
2599 'date': message['date'],
2600 'lgtm': False,
2601 'message': message['text'],
2602 'not_lgtm': False,
2603 'sender': message['sender'],
2604 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002605 if message['disapproval']:
2606 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002607 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002608 elif message['approval']:
2609 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002610 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002611 elif message['sender'] == data['owner_email']:
2612 color = Fore.MAGENTA
2613 else:
2614 color = Fore.BLUE
2615 print '\n%s%s %s%s' % (
2616 color, message['date'].split('.', 1)[0], message['sender'],
2617 Fore.RESET)
2618 if message['text'].strip():
2619 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002620 if options.json_file:
2621 with open(options.json_file, 'wb') as f:
2622 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002623 return 0
2624
2625
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002626def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002627 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002628 parser.add_option('-d', '--display', action='store_true',
2629 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002630 auth.add_auth_options(parser)
2631 options, _ = parser.parse_args(args)
2632 auth_config = auth.extract_auth_config_from_options(options)
2633 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002634 if not cl.GetIssue():
2635 DieWithError('This branch has no associated changelist.')
2636 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002637 if options.display:
2638 print description.description
2639 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002640 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002641 if cl.GetDescription() != description.description:
2642 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002643 return 0
2644
2645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002646def CreateDescriptionFromLog(args):
2647 """Pulls out the commit log to use as a base for the CL description."""
2648 log_args = []
2649 if len(args) == 1 and not args[0].endswith('.'):
2650 log_args = [args[0] + '..']
2651 elif len(args) == 1 and args[0].endswith('...'):
2652 log_args = [args[0][:-1]]
2653 elif len(args) == 2:
2654 log_args = [args[0] + '..' + args[1]]
2655 else:
2656 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002657 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002658
2659
thestig@chromium.org44202a22014-03-11 19:22:18 +00002660def CMDlint(parser, args):
2661 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002662 parser.add_option('--filter', action='append', metavar='-x,+y',
2663 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002664 auth.add_auth_options(parser)
2665 options, args = parser.parse_args(args)
2666 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002667
2668 # Access to a protected member _XX of a client class
2669 # pylint: disable=W0212
2670 try:
2671 import cpplint
2672 import cpplint_chromium
2673 except ImportError:
2674 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2675 return 1
2676
2677 # Change the current working directory before calling lint so that it
2678 # shows the correct base.
2679 previous_cwd = os.getcwd()
2680 os.chdir(settings.GetRoot())
2681 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002682 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002683 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2684 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002685 if not files:
2686 print "Cannot lint an empty CL"
2687 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002688
2689 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002690 command = args + files
2691 if options.filter:
2692 command = ['--filter=' + ','.join(options.filter)] + command
2693 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002694
2695 white_regex = re.compile(settings.GetLintRegex())
2696 black_regex = re.compile(settings.GetLintIgnoreRegex())
2697 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2698 for filename in filenames:
2699 if white_regex.match(filename):
2700 if black_regex.match(filename):
2701 print "Ignoring file %s" % filename
2702 else:
2703 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2704 extra_check_functions)
2705 else:
2706 print "Skipping file %s" % filename
2707 finally:
2708 os.chdir(previous_cwd)
2709 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2710 if cpplint._cpplint_state.error_count != 0:
2711 return 1
2712 return 0
2713
2714
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002715def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002716 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002717 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002718 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002719 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002720 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002721 auth.add_auth_options(parser)
2722 options, args = parser.parse_args(args)
2723 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002724
sbc@chromium.org71437c02015-04-09 19:29:40 +00002725 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002726 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727 return 1
2728
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002729 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002730 if args:
2731 base_branch = args[0]
2732 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002733 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002734 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002735
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002736 cl.RunHook(
2737 committing=not options.upload,
2738 may_prompt=False,
2739 verbose=options.verbose,
2740 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002741 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002742
2743
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002744def AddChangeIdToCommitMessage(options, args):
2745 """Re-commits using the current message, assumes the commit hook is in
2746 place.
2747 """
2748 log_desc = options.message or CreateDescriptionFromLog(args)
2749 git_command = ['commit', '--amend', '-m', log_desc]
2750 RunGit(git_command)
2751 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002752 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002753 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002754 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002755 else:
2756 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2757
2758
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002759def GenerateGerritChangeId(message):
2760 """Returns Ixxxxxx...xxx change id.
2761
2762 Works the same way as
2763 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2764 but can be called on demand on all platforms.
2765
2766 The basic idea is to generate git hash of a state of the tree, original commit
2767 message, author/committer info and timestamps.
2768 """
2769 lines = []
2770 tree_hash = RunGitSilent(['write-tree'])
2771 lines.append('tree %s' % tree_hash.strip())
2772 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2773 if code == 0:
2774 lines.append('parent %s' % parent.strip())
2775 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2776 lines.append('author %s' % author.strip())
2777 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2778 lines.append('committer %s' % committer.strip())
2779 lines.append('')
2780 # Note: Gerrit's commit-hook actually cleans message of some lines and
2781 # whitespace. This code is not doing this, but it clearly won't decrease
2782 # entropy.
2783 lines.append(message)
2784 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2785 stdin='\n'.join(lines))
2786 return 'I%s' % change_hash.strip()
2787
2788
piman@chromium.org336f9122014-09-04 02:16:55 +00002789def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002790 """upload the current branch to gerrit."""
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002791 # TODO(tandrii): refactor this to be a method of _GerritChangelistImpl,
2792 # to avoid private members accessors below.
2793
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002794 # We assume the remote called "origin" is the one we want.
2795 # It is probably not worthwhile to support different workflows.
2796 gerrit_remote = 'origin'
2797
luqui@chromium.org609f3952015-05-04 22:47:04 +00002798 remote, remote_branch = cl.GetRemoteBranch()
2799 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2800 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002801
andybons@chromium.org962f9462016-02-03 20:00:42 +00002802 if options.title:
2803 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002804 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002805
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002806 if options.squash:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002807 if not cl.GetIssue():
2808 # TODO(tandrii): deperecate this after 2016Q2.
2809 # Backwards compatibility with shadow branch, which used to contain
2810 # change-id for a given branch, using which we can fetch actual issue
2811 # number and set it as the property of the branch, which is the new way.
2812 message = RunGitSilent(['show', '--format=%B', '-s',
2813 'refs/heads/git_cl_uploads/%s' % cl.GetBranch()])
2814 if message:
2815 change_ids = git_footers.get_footer_change_id(message.strip())
2816 if change_ids and len(change_ids) == 1:
2817 details = gerrit_util.GetChangeDetail(
2818 cl._codereview_impl._GetGerritHost(), change_ids[0])
2819 if details:
2820 print('WARNING: found old upload in branch git_cl_uploads/%s '
2821 'corresponding to issue %s' %
2822 (cl.GetBranch(), details['_number']))
2823 cl.SetIssue(details['_number'])
2824 if not cl.GetIssue():
2825 DieWithError(
2826 '\n' # For readability of the blob below.
2827 'Found old upload in branch git_cl_uploads/%s, '
2828 'but failed to find corresponding Gerrit issue.\n'
2829 'If you know the issue number, set it manually first:\n'
2830 ' git cl issue 123456\n'
2831 'If you intended to upload this CL as new issue, '
2832 'just delete or rename the old upload branch:\n'
2833 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2834 'After that, please run git cl upload again.' %
2835 tuple([cl.GetBranch()] * 3))
2836 # End of backwards compatability.
2837
2838 if cl.GetIssue():
2839 # Try to get the message from a previous upload.
2840 message = cl.GetDescription()
2841 if not message:
2842 DieWithError(
2843 'failed to fetch description from current Gerrit issue %d\n'
2844 '%s' % (cl.GetIssue(), cl.GetIssueURL()))
2845 change_id = cl._codereview_impl._GetChangeDetail([])['change_id']
2846 while True:
2847 footer_change_ids = git_footers.get_footer_change_id(message)
2848 if footer_change_ids == [change_id]:
2849 break
2850 if not footer_change_ids:
2851 message = git_footers.add_footer_change_id(message, change_id)
2852 print('WARNING: appended missing Change-Id to issue description')
2853 continue
2854 # There is already a valid footer but with different or several ids.
2855 # Doing this automatically is non-trivial as we don't want to lose
2856 # existing other footers, yet we want to append just 1 desired
2857 # Change-Id. Thus, just create a new footer, but let user verify the new
2858 # description.
2859 message = '%s\n\nChange-Id: %s' % (message, change_id)
2860 print(
2861 'WARNING: issue %s has Change-Id footer(s):\n'
2862 ' %s\n'
2863 'but issue has Change-Id %s, according to Gerrit.\n'
2864 'Please, check the proposed correction to the description, '
2865 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2866 % (cl.GetIssue(), '\n '.join(footer_change_ids), change_id,
2867 change_id))
2868 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2869 if not options.force:
2870 change_desc = ChangeDescription(message)
2871 change_desc.prompt()
2872 message = change_desc.description
2873 if not message:
2874 DieWithError("Description is empty. Aborting...")
2875 # Continue the while loop.
2876 # Sanity check of this code - we should end up with proper message footer.
2877 assert [change_id] == git_footers.get_footer_change_id(message)
2878 change_desc = ChangeDescription(message)
2879 else:
2880 change_desc = ChangeDescription(
2881 options.message or CreateDescriptionFromLog(args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002882 if not options.force:
2883 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002884 if not change_desc.description:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002885 DieWithError("Description is empty. Aborting...")
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002886 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002887 change_ids = git_footers.get_footer_change_id(message)
2888 if len(change_ids) > 1:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002889 DieWithError('too many Change-Id footers, at most 1 allowed.')
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002890 if not change_ids:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002891 # Generate the Change-Id automatically.
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002892 message = git_footers.add_footer_change_id(
2893 message, GenerateGerritChangeId(message))
2894 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002895 change_ids = git_footers.get_footer_change_id(message)
2896 assert len(change_ids) == 1
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002897 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002898
2899 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2900 if remote is '.':
2901 # If our upstream branch is local, we base our squashed commit on its
2902 # squashed version.
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002903 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2904 # Check the squashed hash of the parent.
2905 parent = RunGit(['config',
2906 'branch.%s.gerritsquashhash' % upstream_branch_name],
2907 error_ok=True).strip()
2908 # Verify that the upstream branch has been uploaded too, otherwise
2909 # Gerrit will create additional CLs when uploading.
2910 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2911 RunGitSilent(['rev-parse', parent + ':'])):
2912 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2913 DieWithError(
2914 'Upload upstream branch %s first.\n'
2915 'Note: maybe you\'ve uploaded it with --no-squash or with an old\n'
2916 ' version of depot_tools. If so, then re-upload it with:\n'
2917 ' git cl upload --squash\n' % upstream_branch_name)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002918 else:
2919 parent = cl.GetCommonAncestorWithUpstream()
2920
2921 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2922 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2923 '-m', message]).strip()
2924 else:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002925 change_desc = ChangeDescription(
2926 options.message or CreateDescriptionFromLog(args))
2927 if not change_desc.description:
2928 DieWithError("Description is empty. Aborting...")
2929
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002930 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002931 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002932 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002933 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002934 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002935 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002936
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002937 assert change_desc
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002938 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2939 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002940 if len(commits) > 1:
2941 print('WARNING: This will upload %d commits. Run the following command '
2942 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002943 print('git log %s..%s' % (parent, ref_to_push))
2944 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002945 'commit.')
2946 ask_for_data('About to upload; enter to confirm.')
2947
piman@chromium.org336f9122014-09-04 02:16:55 +00002948 if options.reviewers or options.tbr_owners:
2949 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002950
ukai@chromium.orge8077812012-02-03 03:41:46 +00002951 receive_options = []
2952 cc = cl.GetCCList().split(',')
2953 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002954 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002955 cc = filter(None, cc)
2956 if cc:
2957 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002958 if change_desc.get_reviewers():
2959 receive_options.extend(
2960 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002961
ukai@chromium.orge8077812012-02-03 03:41:46 +00002962 git_command = ['push']
2963 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002964 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002965 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002966 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002967 push_stdout = gclient_utils.CheckCallAndFilter(
2968 ['git'] + git_command,
2969 print_stdout=True,
2970 # Flush after every line: useful for seeing progress when running as
2971 # recipe.
2972 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002973
2974 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002975 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2976 change_numbers = [m.group(1)
2977 for m in map(regex.match, push_stdout.splitlines())
2978 if m]
2979 if len(change_numbers) != 1:
2980 DieWithError(
2981 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2982 'Change-Id: %s') % (len(change_numbers), change_id))
2983 cl.SetIssue(change_numbers[0])
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002984 RunGit(['config', 'branch.%s.gerritsquashhash' % cl.GetBranch(),
2985 ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002986 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002987
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002988
wittman@chromium.org455dc922015-01-26 20:15:50 +00002989def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2990 """Computes the remote branch ref to use for the CL.
2991
2992 Args:
2993 remote (str): The git remote for the CL.
2994 remote_branch (str): The git remote branch for the CL.
2995 target_branch (str): The target branch specified by the user.
2996 pending_prefix (str): The pending prefix from the settings.
2997 """
2998 if not (remote and remote_branch):
2999 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003000
wittman@chromium.org455dc922015-01-26 20:15:50 +00003001 if target_branch:
3002 # Cannonicalize branch references to the equivalent local full symbolic
3003 # refs, which are then translated into the remote full symbolic refs
3004 # below.
3005 if '/' not in target_branch:
3006 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3007 else:
3008 prefix_replacements = (
3009 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3010 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3011 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3012 )
3013 match = None
3014 for regex, replacement in prefix_replacements:
3015 match = re.search(regex, target_branch)
3016 if match:
3017 remote_branch = target_branch.replace(match.group(0), replacement)
3018 break
3019 if not match:
3020 # This is a branch path but not one we recognize; use as-is.
3021 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003022 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3023 # Handle the refs that need to land in different refs.
3024 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003025
wittman@chromium.org455dc922015-01-26 20:15:50 +00003026 # Create the true path to the remote branch.
3027 # Does the following translation:
3028 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3029 # * refs/remotes/origin/master -> refs/heads/master
3030 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3031 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3032 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3033 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3034 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3035 'refs/heads/')
3036 elif remote_branch.startswith('refs/remotes/branch-heads'):
3037 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3038 # If a pending prefix exists then replace refs/ with it.
3039 if pending_prefix:
3040 remote_branch = remote_branch.replace('refs/', pending_prefix)
3041 return remote_branch
3042
3043
piman@chromium.org336f9122014-09-04 02:16:55 +00003044def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003045 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003046 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003047 upload_args.extend(['--server', cl.GetCodereviewServer()])
3048 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
3049 upload_args.extend(auth.auth_config_to_command_options(
3050 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003051 if options.emulate_svn_auto_props:
3052 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003053
3054 change_desc = None
3055
pgervais@chromium.org91141372014-01-09 23:27:20 +00003056 if options.email is not None:
3057 upload_args.extend(['--email', options.email])
3058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003060 if options.title:
3061 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00003062 if options.message:
3063 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00003064 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003065 print ("This branch is associated with issue %s. "
3066 "Adding patch to that issue." % cl.GetIssue())
3067 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003068 if options.title:
3069 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00003070 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003071 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00003072 if options.reviewers or options.tbr_owners:
3073 change_desc.update_reviewers(options.reviewers,
3074 options.tbr_owners,
3075 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003076 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003078
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003080 print "Description is empty; aborting."
3081 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003082
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003083 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003084 if change_desc.get_reviewers():
3085 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00003086 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003087 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00003088 DieWithError("Must specify reviewers to send email.")
3089 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00003090
3091 # We check this before applying rietveld.private assuming that in
3092 # rietveld.cc only addresses which we can send private CLs to are listed
3093 # if rietveld.private is set, and so we should ignore rietveld.cc only when
3094 # --private is specified explicitly on the command line.
3095 if options.private:
3096 logging.warn('rietveld.cc is ignored since private flag is specified. '
3097 'You need to review and add them manually if necessary.')
3098 cc = cl.GetCCListWithoutDefault()
3099 else:
3100 cc = cl.GetCCList()
3101 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00003102 if cc:
3103 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003104
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003105 if options.private or settings.GetDefaultPrivateFlag() == "True":
3106 upload_args.append('--private')
3107
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003108 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00003109 if not options.find_copies:
3110 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003111
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003112 # Include the upstream repo's URL in the change -- this is useful for
3113 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003114 remote_url = cl.GetGitBaseUrlFromConfig()
3115 if not remote_url:
3116 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003117 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003118 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00003119 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
3120 remote_url = (cl.GetRemoteUrl() + '@'
3121 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122 if remote_url:
3123 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00003124 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00003125 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
3126 settings.GetPendingRefPrefix())
3127 if target_ref:
3128 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003129
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003130 # Look for dependent patchsets. See crbug.com/480453 for more details.
3131 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3132 upstream_branch = ShortBranchName(upstream_branch)
3133 if remote is '.':
3134 # A local branch is being tracked.
3135 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00003136 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003137 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003138 print ('Skipping dependency patchset upload because git config '
3139 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003140 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003141 else:
3142 auth_config = auth.extract_auth_config_from_options(options)
3143 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
3144 branch_cl_issue_url = branch_cl.GetIssueURL()
3145 branch_cl_issue = branch_cl.GetIssue()
3146 branch_cl_patchset = branch_cl.GetPatchset()
3147 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
3148 upload_args.extend(
3149 ['--depends_on_patchset', '%s:%s' % (
3150 branch_cl_issue, branch_cl_patchset)])
3151 print
3152 print ('The current branch (%s) is tracking a local branch (%s) with '
3153 'an associated CL.') % (cl.GetBranch(), local_branch)
3154 print 'Adding %s/#ps%s as a dependency patchset.' % (
3155 branch_cl_issue_url, branch_cl_patchset)
3156 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003157
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003158 project = settings.GetProject()
3159 if project:
3160 upload_args.extend(['--project', project])
3161
rmistry@google.comef966222015-04-07 11:15:01 +00003162 if options.cq_dry_run:
3163 upload_args.extend(['--cq_dry_run'])
3164
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003165 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00003166 upload_args = ['upload'] + upload_args + args
3167 logging.info('upload.RealMain(%s)', upload_args)
3168 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00003169 issue = int(issue)
3170 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00003171 except KeyboardInterrupt:
3172 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003173 except:
3174 # If we got an exception after the user typed a description for their
3175 # change, back up the description before re-raising.
3176 if change_desc:
3177 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
3178 print '\nGot exception while uploading -- saving description to %s\n' \
3179 % backup_path
3180 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003181 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003182 backup_file.close()
3183 raise
3184
3185 if not cl.GetIssue():
3186 cl.SetIssue(issue)
3187 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003188
3189 if options.use_commit_queue:
3190 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191 return 0
3192
3193
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003194def cleanup_list(l):
3195 """Fixes a list so that comma separated items are put as individual items.
3196
3197 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3198 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3199 """
3200 items = sum((i.split(',') for i in l), [])
3201 stripped_items = (i.strip() for i in items)
3202 return sorted(filter(None, stripped_items))
3203
3204
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003205@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003206def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003207 """Uploads the current changelist to codereview.
3208
3209 Can skip dependency patchset uploads for a branch by running:
3210 git config branch.branch_name.skip-deps-uploads True
3211 To unset run:
3212 git config --unset branch.branch_name.skip-deps-uploads
3213 Can also set the above globally by using the --global flag.
3214 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003215 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3216 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003217 parser.add_option('--bypass-watchlists', action='store_true',
3218 dest='bypass_watchlists',
3219 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003220 parser.add_option('-f', action='store_true', dest='force',
3221 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003222 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003223 parser.add_option('-t', dest='title',
3224 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003225 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003226 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003227 help='reviewer email addresses')
3228 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003229 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003230 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003231 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003232 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003233 parser.add_option('--emulate_svn_auto_props',
3234 '--emulate-svn-auto-props',
3235 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003236 dest="emulate_svn_auto_props",
3237 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003238 parser.add_option('-c', '--use-commit-queue', action='store_true',
3239 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003240 parser.add_option('--private', action='store_true',
3241 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003242 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003243 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003244 metavar='TARGET',
3245 help='Apply CL to remote ref TARGET. ' +
3246 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003247 parser.add_option('--squash', action='store_true',
3248 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003249 parser.add_option('--no-squash', action='store_true',
3250 help='Don\'t squash multiple commits into one ' +
3251 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003252 parser.add_option('--email', default=None,
3253 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003254 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3255 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003256 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3257 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003258 help='Send the patchset to do a CQ dry run right after '
3259 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003260 parser.add_option('--dependencies', action='store_true',
3261 help='Uploads CLs of all the local branches that depend on '
3262 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003263
rmistry@google.com2dd99862015-06-22 12:22:18 +00003264 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003265 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003266 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003267 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003268 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003269
sbc@chromium.org71437c02015-04-09 19:29:40 +00003270 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003271 return 1
3272
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003273 options.reviewers = cleanup_list(options.reviewers)
3274 options.cc = cleanup_list(options.cc)
3275
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003276 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3277 settings.GetIsGerrit()
3278
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003279 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003280 if args:
3281 # TODO(ukai): is it ok for gerrit case?
3282 base_branch = args[0]
3283 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003284 if cl.GetBranch() is None:
3285 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3286
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003287 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003288 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003289 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003290
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003291 # Make sure authenticated to Rietveld before running expensive hooks. It is
3292 # a fast, best efforts check. Rietveld still can reject the authentication
3293 # during the actual upload.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003294 if not cl.IsGerrit() and auth_config.use_oauth2:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003295 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003296 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003297 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003298 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003299
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003300 # Apply watchlists on upload.
3301 change = cl.GetChange(base_branch, None)
3302 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3303 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003304 if not options.bypass_watchlists:
3305 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003306
ukai@chromium.orge8077812012-02-03 03:41:46 +00003307 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003308 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003309 # Set the reviewer list now so that presubmit checks can access it.
3310 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003311 change_description.update_reviewers(options.reviewers,
3312 options.tbr_owners,
3313 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003314 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003315 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003316 may_prompt=not options.force,
3317 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003318 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003319 if not hook_results.should_continue():
3320 return 1
3321 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003322 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003323
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003324 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003325 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003326 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003327 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003328 print ('The last upload made from this repository was patchset #%d but '
3329 'the most recent patchset on the server is #%d.'
3330 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003331 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3332 'from another machine or branch the patch you\'re uploading now '
3333 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003334 ask_for_data('About to upload; enter to confirm.')
3335
iannucci@chromium.org79540052012-10-19 23:15:26 +00003336 print_stats(options.similarity, options.find_copies, args)
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003337 if cl.IsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003338 if options.squash and options.no_squash:
3339 DieWithError('Can only use one of --squash or --no-squash')
3340
3341 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3342 not options.no_squash)
3343
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00003344 ret = GerritUpload(options, args, cl, change)
3345 else:
3346 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003347 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003348 git_set_branch_value('last-upload-hash',
3349 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003350 # Run post upload hooks, if specified.
3351 if settings.GetRunPostUploadHook():
3352 presubmit_support.DoPostUploadExecuter(
3353 change,
3354 cl,
3355 settings.GetRoot(),
3356 options.verbose,
3357 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003358
rmistry@google.com2dd99862015-06-22 12:22:18 +00003359 # Upload all dependencies if specified.
3360 if options.dependencies:
3361 print
3362 print '--dependencies has been specified.'
3363 print 'All dependent local branches will be re-uploaded.'
3364 print
3365 # Remove the dependencies flag from args so that we do not end up in a
3366 # loop.
3367 orig_args.remove('--dependencies')
3368 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003369 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003370
3371
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003372def IsSubmoduleMergeCommit(ref):
3373 # When submodules are added to the repo, we expect there to be a single
3374 # non-git-svn merge commit at remote HEAD with a signature comment.
3375 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003376 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003377 return RunGit(cmd) != ''
3378
3379
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003380def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003381 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003382
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003383 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3384 upstream and closes the issue automatically and atomically.
3385
3386 Otherwise (in case of Rietveld):
3387 Squashes branch into a single commit.
3388 Updates changelog with metadata (e.g. pointer to review).
3389 Pushes/dcommits the code upstream.
3390 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003391 """
3392 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3393 help='bypass upload presubmit hook')
3394 parser.add_option('-m', dest='message',
3395 help="override review description")
3396 parser.add_option('-f', action='store_true', dest='force',
3397 help="force yes to questions (don't prompt)")
3398 parser.add_option('-c', dest='contributor',
3399 help="external contributor for patch (appended to " +
3400 "description and used as author for git). Should be " +
3401 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003402 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003403 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003404 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003405 auth_config = auth.extract_auth_config_from_options(options)
3406
3407 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003409 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3410 if cl.IsGerrit():
3411 if options.message:
3412 # This could be implemented, but it requires sending a new patch to
3413 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3414 # Besides, Gerrit has the ability to change the commit message on submit
3415 # automatically, thus there is no need to support this option (so far?).
3416 parser.error('-m MESSAGE option is not supported for Gerrit.')
3417 if options.contributor:
3418 parser.error(
3419 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3420 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3421 'the contributor\'s "name <email>". If you can\'t upload such a '
3422 'commit for review, contact your repository admin and request'
3423 '"Forge-Author" permission.')
3424 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3425 options.verbose)
3426
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003427 current = cl.GetBranch()
3428 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3429 if not settings.GetIsGitSvn() and remote == '.':
3430 print
3431 print 'Attempting to push branch %r into another local branch!' % current
3432 print
3433 print 'Either reparent this branch on top of origin/master:'
3434 print ' git reparent-branch --root'
3435 print
3436 print 'OR run `git rebase-update` if you think the parent branch is already'
3437 print 'committed.'
3438 print
3439 print ' Current parent: %r' % upstream_branch
3440 return 1
3441
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003442 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003443 # Default to merging against our best guess of the upstream branch.
3444 args = [cl.GetUpstreamBranch()]
3445
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003446 if options.contributor:
3447 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3448 print "Please provide contibutor as 'First Last <email@example.com>'"
3449 return 1
3450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003451 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003452 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003453
sbc@chromium.org71437c02015-04-09 19:29:40 +00003454 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003455 return 1
3456
3457 # This rev-list syntax means "show all commits not in my branch that
3458 # are in base_branch".
3459 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3460 base_branch]).splitlines()
3461 if upstream_commits:
3462 print ('Base branch "%s" has %d commits '
3463 'not in this branch.' % (base_branch, len(upstream_commits)))
3464 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3465 return 1
3466
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003467 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003468 svn_head = None
3469 if cmd == 'dcommit' or base_has_submodules:
3470 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3471 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003472
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003473 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003474 # If the base_head is a submodule merge commit, the first parent of the
3475 # base_head should be a git-svn commit, which is what we're interested in.
3476 base_svn_head = base_branch
3477 if base_has_submodules:
3478 base_svn_head += '^1'
3479
3480 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003481 if extra_commits:
3482 print ('This branch has %d additional commits not upstreamed yet.'
3483 % len(extra_commits.splitlines()))
3484 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3485 'before attempting to %s.' % (base_branch, cmd))
3486 return 1
3487
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003488 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003489 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003490 author = None
3491 if options.contributor:
3492 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003493 hook_results = cl.RunHook(
3494 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003495 may_prompt=not options.force,
3496 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003497 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003498 if not hook_results.should_continue():
3499 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003500
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003501 # Check the tree status if the tree status URL is set.
3502 status = GetTreeStatus()
3503 if 'closed' == status:
3504 print('The tree is closed. Please wait for it to reopen. Use '
3505 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3506 return 1
3507 elif 'unknown' == status:
3508 print('Unable to determine tree status. Please verify manually and '
3509 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3510 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003511
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003512 change_desc = ChangeDescription(options.message)
3513 if not change_desc.description and cl.GetIssue():
3514 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003515
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003516 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003517 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003518 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003519 else:
3520 print 'No description set.'
3521 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3522 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003524 # Keep a separate copy for the commit message, because the commit message
3525 # contains the link to the Rietveld issue, while the Rietveld message contains
3526 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003527 # Keep a separate copy for the commit message.
3528 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003529 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003530
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003531 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003532 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003533 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003534 # after it. Add a period on a new line to circumvent this. Also add a space
3535 # before the period to make sure that Gitiles continues to correctly resolve
3536 # the URL.
3537 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003538 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003539 commit_desc.append_footer('Patch from %s.' % options.contributor)
3540
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003541 print('Description:')
3542 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003543
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003544 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003546 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003548 # We want to squash all this branch's commits into one commit with the proper
3549 # description. We do this by doing a "reset --soft" to the base branch (which
3550 # keeps the working copy the same), then dcommitting that. If origin/master
3551 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3552 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003553 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003554 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3555 # Delete the branches if they exist.
3556 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3557 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3558 result = RunGitWithCode(showref_cmd)
3559 if result[0] == 0:
3560 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561
3562 # We might be in a directory that's present in this branch but not in the
3563 # trunk. Move up to the top of the tree so that git commands that expect a
3564 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003565 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003566 if rel_base_path:
3567 os.chdir(rel_base_path)
3568
3569 # Stuff our change into the merge branch.
3570 # We wrap in a try...finally block so if anything goes wrong,
3571 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003572 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003573 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003574 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003575 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003576 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003577 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003578 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003579 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003580 RunGit(
3581 [
3582 'commit', '--author', options.contributor,
3583 '-m', commit_desc.description,
3584 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003585 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003586 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003587 if base_has_submodules:
3588 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3589 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3590 RunGit(['checkout', CHERRY_PICK_BRANCH])
3591 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003592 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003593 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003594 mirror = settings.GetGitMirror(remote)
3595 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003596 pending_prefix = settings.GetPendingRefPrefix()
3597 if not pending_prefix or branch.startswith(pending_prefix):
3598 # If not using refs/pending/heads/* at all, or target ref is already set
3599 # to pending, then push to the target ref directly.
3600 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003601 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003602 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003603 else:
3604 # Cherry-pick the change on top of pending ref and then push it.
3605 assert branch.startswith('refs/'), branch
3606 assert pending_prefix[-1] == '/', pending_prefix
3607 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003608 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003609 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003610 if retcode == 0:
3611 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612 else:
3613 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003614 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003615 'svn', 'dcommit',
3616 '-C%s' % options.similarity,
3617 '--no-rebase', '--rmdir',
3618 ]
3619 if settings.GetForceHttpsCommitUrl():
3620 # Allow forcing https commit URLs for some projects that don't allow
3621 # committing to http URLs (like Google Code).
3622 remote_url = cl.GetGitSvnRemoteUrl()
3623 if urlparse.urlparse(remote_url).scheme == 'http':
3624 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003625 cmd_args.append('--commit-url=%s' % remote_url)
3626 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003627 if 'Committed r' in output:
3628 revision = re.match(
3629 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3630 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003631 finally:
3632 # And then swap back to the original branch and clean up.
3633 RunGit(['checkout', '-q', cl.GetBranch()])
3634 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003635 if base_has_submodules:
3636 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003637
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003638 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003639 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003640 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003641
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003642 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003643 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003644 try:
3645 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3646 # We set pushed_to_pending to False, since it made it all the way to the
3647 # real ref.
3648 pushed_to_pending = False
3649 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003650 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003652 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003653 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003654 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003655 if not to_pending:
3656 if viewvc_url and revision:
3657 change_desc.append_footer(
3658 'Committed: %s%s' % (viewvc_url, revision))
3659 elif revision:
3660 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661 print ('Closing issue '
3662 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003663 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003665 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003666 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003667 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003668 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003669 if options.bypass_hooks:
3670 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3671 else:
3672 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003673 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003674 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003675
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003676 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003677 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3678 print 'The commit is in the pending queue (%s).' % pending_ref
3679 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003680 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003681 'footer.' % branch)
3682
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003683 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3684 if os.path.isfile(hook):
3685 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003686
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003687 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003688
3689
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003690def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3691 print
3692 print 'Waiting for commit to be landed on %s...' % real_ref
3693 print '(If you are impatient, you may Ctrl-C once without harm)'
3694 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3695 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003696 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003697
3698 loop = 0
3699 while True:
3700 sys.stdout.write('fetching (%d)... \r' % loop)
3701 sys.stdout.flush()
3702 loop += 1
3703
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003704 if mirror:
3705 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003706 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3707 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3708 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3709 for commit in commits.splitlines():
3710 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3711 print 'Found commit on %s' % real_ref
3712 return commit
3713
3714 current_rev = to_rev
3715
3716
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003717def PushToGitPending(remote, pending_ref, upstream_ref):
3718 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3719
3720 Returns:
3721 (retcode of last operation, output log of last operation).
3722 """
3723 assert pending_ref.startswith('refs/'), pending_ref
3724 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3725 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3726 code = 0
3727 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003728 max_attempts = 3
3729 attempts_left = max_attempts
3730 while attempts_left:
3731 if attempts_left != max_attempts:
3732 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3733 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003734
3735 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003736 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003737 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003738 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003739 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003740 print 'Fetch failed with exit code %d.' % code
3741 if out.strip():
3742 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003743 continue
3744
3745 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003746 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003747 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003748 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003749 if code:
3750 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003751 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3752 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003753 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3754 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003755 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003756 return code, out
3757
3758 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003759 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003760 code, out = RunGitWithCode(
3761 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3762 if code == 0:
3763 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003764 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003765 return code, out
3766
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003767 print 'Push failed with exit code %d.' % code
3768 if out.strip():
3769 print out.strip()
3770 if IsFatalPushFailure(out):
3771 print (
3772 'Fatal push error. Make sure your .netrc credentials and git '
3773 'user.email are correct and you have push access to the repo.')
3774 return code, out
3775
3776 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003777 return code, out
3778
3779
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003780def IsFatalPushFailure(push_stdout):
3781 """True if retrying push won't help."""
3782 return '(prohibited by Gerrit)' in push_stdout
3783
3784
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003785@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003786def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003787 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003788 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003789 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003790 # If it looks like previous commits were mirrored with git-svn.
3791 message = """This repository appears to be a git-svn mirror, but no
3792upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3793 else:
3794 message = """This doesn't appear to be an SVN repository.
3795If your project has a true, writeable git repository, you probably want to run
3796'git cl land' instead.
3797If your project has a git mirror of an upstream SVN master, you probably need
3798to run 'git svn init'.
3799
3800Using the wrong command might cause your commit to appear to succeed, and the
3801review to be closed, without actually landing upstream. If you choose to
3802proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003803 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003804 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003805 return SendUpstream(parser, args, 'dcommit')
3806
3807
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003808@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003809def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003810 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003811 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003812 print('This appears to be an SVN repository.')
3813 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003814 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003815 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003816 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003817
3818
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003819@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003821 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003822 parser.add_option('-b', dest='newbranch',
3823 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003824 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003825 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003826 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3827 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003828 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003829 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003830 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003831 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003832 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003833 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003834
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003835
3836 group = optparse.OptionGroup(
3837 parser,
3838 'Options for continuing work on the current issue uploaded from a '
3839 'different clone (e.g. different machine). Must be used independently '
3840 'from the other options. No issue number should be specified, and the '
3841 'branch must have an issue number associated with it')
3842 group.add_option('--reapply', action='store_true', dest='reapply',
3843 help='Reset the branch and reapply the issue.\n'
3844 'CAUTION: This will undo any local changes in this '
3845 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003846
3847 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003848 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003849 parser.add_option_group(group)
3850
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003851 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003852 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003853 auth_config = auth.extract_auth_config_from_options(options)
3854
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003855 cl = Changelist(auth_config=auth_config)
3856
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003857 issue_arg = None
3858 if options.reapply :
3859 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003860 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003861
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003862 issue_arg = cl.GetIssue()
3863 upstream = cl.GetUpstreamBranch()
3864 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003865 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003866
3867 RunGit(['reset', '--hard', upstream])
3868 if options.pull:
3869 RunGit(['pull'])
3870 else:
3871 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003872 parser.error('Must specify issue number or url')
3873 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003874
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003875 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003876 parser.print_help()
3877 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003878
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003879 if cl.IsGerrit():
3880 if options.reject:
3881 parser.error('--reject is not supported with Gerrit codereview.')
3882 if options.nocommit:
3883 parser.error('--nocommit is not supported with Gerrit codereview.')
3884 if options.directory:
3885 parser.error('--directory is not supported with Gerrit codereview.')
3886
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003887 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003888 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003889 return 1
3890
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003891 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003892 if options.reapply:
3893 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003894 if options.force:
3895 RunGit(['branch', '-D', options.newbranch],
3896 stderr=subprocess2.PIPE, error_ok=True)
3897 RunGit(['checkout', '-b', options.newbranch,
3898 Changelist().GetUpstreamBranch()])
3899
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003900 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3901 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902
3903
3904def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003905 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003906 # Provide a wrapper for git svn rebase to help avoid accidental
3907 # git svn dcommit.
3908 # It's the only command that doesn't use parser at all since we just defer
3909 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003910
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003911 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912
3913
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003914def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003915 """Fetches the tree status and returns either 'open', 'closed',
3916 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003917 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003918 if url:
3919 status = urllib2.urlopen(url).read().lower()
3920 if status.find('closed') != -1 or status == '0':
3921 return 'closed'
3922 elif status.find('open') != -1 or status == '1':
3923 return 'open'
3924 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003925 return 'unset'
3926
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003928def GetTreeStatusReason():
3929 """Fetches the tree status from a json url and returns the message
3930 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003931 url = settings.GetTreeStatusUrl()
3932 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003933 connection = urllib2.urlopen(json_url)
3934 status = json.loads(connection.read())
3935 connection.close()
3936 return status['message']
3937
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003938
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003939def GetBuilderMaster(bot_list):
3940 """For a given builder, fetch the master from AE if available."""
3941 map_url = 'https://builders-map.appspot.com/'
3942 try:
3943 master_map = json.load(urllib2.urlopen(map_url))
3944 except urllib2.URLError as e:
3945 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3946 (map_url, e))
3947 except ValueError as e:
3948 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3949 if not master_map:
3950 return None, 'Failed to build master map.'
3951
3952 result_master = ''
3953 for bot in bot_list:
3954 builder = bot.split(':', 1)[0]
3955 master_list = master_map.get(builder, [])
3956 if not master_list:
3957 return None, ('No matching master for builder %s.' % builder)
3958 elif len(master_list) > 1:
3959 return None, ('The builder name %s exists in multiple masters %s.' %
3960 (builder, master_list))
3961 else:
3962 cur_master = master_list[0]
3963 if not result_master:
3964 result_master = cur_master
3965 elif result_master != cur_master:
3966 return None, 'The builders do not belong to the same master.'
3967 return result_master, None
3968
3969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003970def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003971 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003972 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003973 status = GetTreeStatus()
3974 if 'unset' == status:
3975 print 'You must configure your tree status URL by running "git cl config".'
3976 return 2
3977
3978 print "The tree is %s" % status
3979 print
3980 print GetTreeStatusReason()
3981 if status != 'open':
3982 return 1
3983 return 0
3984
3985
maruel@chromium.org15192402012-09-06 12:38:29 +00003986def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003987 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003988 group = optparse.OptionGroup(parser, "Try job options")
3989 group.add_option(
3990 "-b", "--bot", action="append",
3991 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3992 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003993 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003994 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003995 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003996 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003997 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003998 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003999 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004000 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004001 "-r", "--revision",
4002 help="Revision to use for the try job; default: the "
4003 "revision will be determined by the try server; see "
4004 "its waterfall for more info")
4005 group.add_option(
4006 "-c", "--clobber", action="store_true", default=False,
4007 help="Force a clobber before building; e.g. don't do an "
4008 "incremental build")
4009 group.add_option(
4010 "--project",
4011 help="Override which project to use. Projects are defined "
4012 "server-side to define what default bot set to use")
4013 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004014 "-p", "--property", dest="properties", action="append", default=[],
4015 help="Specify generic properties in the form -p key1=value1 -p "
4016 "key2=value2 etc (buildbucket only). The value will be treated as "
4017 "json if decodable, or as string otherwise.")
4018 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004019 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004020 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004021 "--use-rietveld", action="store_true", default=False,
4022 help="Use Rietveld to trigger try jobs.")
4023 group.add_option(
4024 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4025 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004026 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004027 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004028 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004029 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004030
machenbach@chromium.org45453142015-09-15 08:45:22 +00004031 if options.use_rietveld and options.properties:
4032 parser.error('Properties can only be specified with buildbucket')
4033
4034 # Make sure that all properties are prop=value pairs.
4035 bad_params = [x for x in options.properties if '=' not in x]
4036 if bad_params:
4037 parser.error('Got properties with missing "=": %s' % bad_params)
4038
maruel@chromium.org15192402012-09-06 12:38:29 +00004039 if args:
4040 parser.error('Unknown arguments: %s' % args)
4041
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004042 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004043 if not cl.GetIssue():
4044 parser.error('Need to upload first')
4045
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004046 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004047 if props.get('closed'):
4048 parser.error('Cannot send tryjobs for a closed CL')
4049
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004050 if props.get('private'):
4051 parser.error('Cannot use trybots with private issue')
4052
maruel@chromium.org15192402012-09-06 12:38:29 +00004053 if not options.name:
4054 options.name = cl.GetBranch()
4055
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004056 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004057 options.master, err_msg = GetBuilderMaster(options.bot)
4058 if err_msg:
4059 parser.error('Tryserver master cannot be found because: %s\n'
4060 'Please manually specify the tryserver master'
4061 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004062
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004063 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004064 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004065 if not options.bot:
4066 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004067
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004068 # Get try masters from PRESUBMIT.py files.
4069 masters = presubmit_support.DoGetTryMasters(
4070 change,
4071 change.LocalPaths(),
4072 settings.GetRoot(),
4073 None,
4074 None,
4075 options.verbose,
4076 sys.stdout)
4077 if masters:
4078 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004079
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004080 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4081 options.bot = presubmit_support.DoGetTrySlaves(
4082 change,
4083 change.LocalPaths(),
4084 settings.GetRoot(),
4085 None,
4086 None,
4087 options.verbose,
4088 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004089
4090 if not options.bot:
4091 # Get try masters from cq.cfg if any.
4092 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4093 # location.
4094 cq_cfg = os.path.join(change.RepositoryRoot(),
4095 'infra', 'config', 'cq.cfg')
4096 if os.path.exists(cq_cfg):
4097 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004098 cq_masters = commit_queue.get_master_builder_map(
4099 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004100 for master, builders in cq_masters.iteritems():
4101 for builder in builders:
4102 # Skip presubmit builders, because these will fail without LGTM.
4103 if 'presubmit' not in builder.lower():
4104 masters.setdefault(master, {})[builder] = ['defaulttests']
4105 if masters:
4106 return masters
4107
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004108 if not options.bot:
4109 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004110
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004111 builders_and_tests = {}
4112 # TODO(machenbach): The old style command-line options don't support
4113 # multiple try masters yet.
4114 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4115 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4116
4117 for bot in old_style:
4118 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004119 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004120 elif ',' in bot:
4121 parser.error('Specify one bot per --bot flag')
4122 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004123 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004124
4125 for bot, tests in new_style:
4126 builders_and_tests.setdefault(bot, []).extend(tests)
4127
4128 # Return a master map with one master to be backwards compatible. The
4129 # master name defaults to an empty string, which will cause the master
4130 # not to be set on rietveld (deprecated).
4131 return {options.master: builders_and_tests}
4132
4133 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004134
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004135 for builders in masters.itervalues():
4136 if any('triggered' in b for b in builders):
4137 print >> sys.stderr, (
4138 'ERROR You are trying to send a job to a triggered bot. This type of'
4139 ' bot requires an\ninitial job from a parent (usually a builder). '
4140 'Instead send your job to the parent.\n'
4141 'Bot list: %s' % builders)
4142 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004143
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004144 patchset = cl.GetMostRecentPatchset()
4145 if patchset and patchset != cl.GetPatchset():
4146 print(
4147 '\nWARNING Mismatch between local config and server. Did a previous '
4148 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4149 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004150 if options.luci:
4151 trigger_luci_job(cl, masters, options)
4152 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004153 try:
4154 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4155 except BuildbucketResponseException as ex:
4156 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004157 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004158 except Exception as e:
4159 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4160 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4161 e, stacktrace)
4162 return 1
4163 else:
4164 try:
4165 cl.RpcServer().trigger_distributed_try_jobs(
4166 cl.GetIssue(), patchset, options.name, options.clobber,
4167 options.revision, masters)
4168 except urllib2.HTTPError as e:
4169 if e.code == 404:
4170 print('404 from rietveld; '
4171 'did you mean to use "git try" instead of "git cl try"?')
4172 return 1
4173 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004174
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004175 for (master, builders) in sorted(masters.iteritems()):
4176 if master:
4177 print 'Master: %s' % master
4178 length = max(len(builder) for builder in builders)
4179 for builder in sorted(builders):
4180 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004181 return 0
4182
4183
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004184def CMDtry_results(parser, args):
4185 group = optparse.OptionGroup(parser, "Try job results options")
4186 group.add_option(
4187 "-p", "--patchset", type=int, help="patchset number if not current.")
4188 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004189 "--print-master", action='store_true', help="print master name as well.")
4190 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004191 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004192 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004193 group.add_option(
4194 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4195 help="Host of buildbucket. The default host is %default.")
4196 parser.add_option_group(group)
4197 auth.add_auth_options(parser)
4198 options, args = parser.parse_args(args)
4199 if args:
4200 parser.error('Unrecognized args: %s' % ' '.join(args))
4201
4202 auth_config = auth.extract_auth_config_from_options(options)
4203 cl = Changelist(auth_config=auth_config)
4204 if not cl.GetIssue():
4205 parser.error('Need to upload first')
4206
4207 if not options.patchset:
4208 options.patchset = cl.GetMostRecentPatchset()
4209 if options.patchset and options.patchset != cl.GetPatchset():
4210 print(
4211 '\nWARNING Mismatch between local config and server. Did a previous '
4212 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4213 'Continuing using\npatchset %s.\n' % options.patchset)
4214 try:
4215 jobs = fetch_try_jobs(auth_config, cl, options)
4216 except BuildbucketResponseException as ex:
4217 print 'Buildbucket error: %s' % ex
4218 return 1
4219 except Exception as e:
4220 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4221 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4222 e, stacktrace)
4223 return 1
4224 print_tryjobs(options, jobs)
4225 return 0
4226
4227
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004228@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004230 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004231 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004232 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004233 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004234
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004235 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004236 if args:
4237 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004238 branch = cl.GetBranch()
4239 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004240 cl = Changelist()
4241 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004242
4243 # Clear configured merge-base, if there is one.
4244 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004245 else:
4246 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004247 return 0
4248
4249
thestig@chromium.org00858c82013-12-02 23:08:03 +00004250def CMDweb(parser, args):
4251 """Opens the current CL in the web browser."""
4252 _, args = parser.parse_args(args)
4253 if args:
4254 parser.error('Unrecognized args: %s' % ' '.join(args))
4255
4256 issue_url = Changelist().GetIssueURL()
4257 if not issue_url:
4258 print >> sys.stderr, 'ERROR No issue to open'
4259 return 1
4260
4261 webbrowser.open(issue_url)
4262 return 0
4263
4264
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004265def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004266 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004267 auth.add_auth_options(parser)
4268 options, args = parser.parse_args(args)
4269 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004270 if args:
4271 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004272 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004273 props = cl.GetIssueProperties()
4274 if props.get('private'):
4275 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004276 cl.SetFlag('commit', '1')
4277 return 0
4278
4279
groby@chromium.org411034a2013-02-26 15:12:01 +00004280def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004281 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004282 auth.add_auth_options(parser)
4283 options, args = parser.parse_args(args)
4284 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004285 if args:
4286 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004287 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004288 # Ensure there actually is an issue to close.
4289 cl.GetDescription()
4290 cl.CloseIssue()
4291 return 0
4292
4293
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004294def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004295 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004296 auth.add_auth_options(parser)
4297 options, args = parser.parse_args(args)
4298 auth_config = auth.extract_auth_config_from_options(options)
4299 if args:
4300 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004301
4302 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004303 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004304 # Staged changes would be committed along with the patch from last
4305 # upload, hence counted toward the "last upload" side in the final
4306 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004307 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004308 return 1
4309
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004310 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004311 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004312 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004313 if not issue:
4314 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004315 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004316 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004317
4318 # Create a new branch based on the merge-base
4319 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00004320 # Update the cached branch in cl instance, to avoid overwriting original
4321 # branch properties.
4322 cl.branch = cl.branchref = None
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004323 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004324 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004325 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004326 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004327 return rtn
4328
wychen@chromium.org06928532015-02-03 02:11:29 +00004329 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004330 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004331 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004332 finally:
4333 RunGit(['checkout', '-q', branch])
4334 RunGit(['branch', '-D', TMP_BRANCH])
4335
4336 return 0
4337
4338
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004339def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004340 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004341 parser.add_option(
4342 '--no-color',
4343 action='store_true',
4344 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004345 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004346 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004347 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004348
4349 author = RunGit(['config', 'user.email']).strip() or None
4350
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004351 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004352
4353 if args:
4354 if len(args) > 1:
4355 parser.error('Unknown args')
4356 base_branch = args[0]
4357 else:
4358 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004359 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004360
4361 change = cl.GetChange(base_branch, None)
4362 return owners_finder.OwnersFinder(
4363 [f.LocalPath() for f in
4364 cl.GetChange(base_branch, None).AffectedFiles()],
4365 change.RepositoryRoot(), author,
4366 fopen=file, os_path=os.path, glob=glob.glob,
4367 disable_color=options.no_color).run()
4368
4369
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004370def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004371 """Generates a diff command."""
4372 # Generate diff for the current branch's changes.
4373 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4374 upstream_commit, '--' ]
4375
4376 if args:
4377 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004378 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004379 diff_cmd.append(arg)
4380 else:
4381 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004382
4383 return diff_cmd
4384
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004385def MatchingFileType(file_name, extensions):
4386 """Returns true if the file name ends with one of the given extensions."""
4387 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004388
enne@chromium.org555cfe42014-01-29 18:21:39 +00004389@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004390def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004391 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004392 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004393 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004394 parser.add_option('--full', action='store_true',
4395 help='Reformat the full content of all touched files')
4396 parser.add_option('--dry-run', action='store_true',
4397 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004398 parser.add_option('--python', action='store_true',
4399 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004400 parser.add_option('--diff', action='store_true',
4401 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004402 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004403
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004404 # git diff generates paths against the root of the repository. Change
4405 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004406 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004407 if rel_base_path:
4408 os.chdir(rel_base_path)
4409
digit@chromium.org29e47272013-05-17 17:01:46 +00004410 # Grab the merge-base commit, i.e. the upstream commit of the current
4411 # branch when it was created or the last time it was rebased. This is
4412 # to cover the case where the user may have called "git fetch origin",
4413 # moving the origin branch to a newer commit, but hasn't rebased yet.
4414 upstream_commit = None
4415 cl = Changelist()
4416 upstream_branch = cl.GetUpstreamBranch()
4417 if upstream_branch:
4418 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4419 upstream_commit = upstream_commit.strip()
4420
4421 if not upstream_commit:
4422 DieWithError('Could not find base commit for this branch. '
4423 'Are you in detached state?')
4424
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004425 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4426 diff_output = RunGit(changed_files_cmd)
4427 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004428 # Filter out files deleted by this CL
4429 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004430
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004431 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4432 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4433 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004434 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004435
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004436 top_dir = os.path.normpath(
4437 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4438
4439 # Locate the clang-format binary in the checkout
4440 try:
4441 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4442 except clang_format.NotFoundError, e:
4443 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004444
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004445 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4446 # formatted. This is used to block during the presubmit.
4447 return_value = 0
4448
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004449 if clang_diff_files:
4450 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004451 cmd = [clang_format_tool]
4452 if not opts.dry_run and not opts.diff:
4453 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004454 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004455 if opts.diff:
4456 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004457 else:
4458 env = os.environ.copy()
4459 env['PATH'] = str(os.path.dirname(clang_format_tool))
4460 try:
4461 script = clang_format.FindClangFormatScriptInChromiumTree(
4462 'clang-format-diff.py')
4463 except clang_format.NotFoundError, e:
4464 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004465
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004466 cmd = [sys.executable, script, '-p0']
4467 if not opts.dry_run and not opts.diff:
4468 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004469
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004470 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4471 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004472
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004473 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4474 if opts.diff:
4475 sys.stdout.write(stdout)
4476 if opts.dry_run and len(stdout) > 0:
4477 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004478
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004479 # Similar code to above, but using yapf on .py files rather than clang-format
4480 # on C/C++ files
4481 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004482 yapf_tool = gclient_utils.FindExecutable('yapf')
4483 if yapf_tool is None:
4484 DieWithError('yapf not found in PATH')
4485
4486 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004487 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004488 cmd = [yapf_tool]
4489 if not opts.dry_run and not opts.diff:
4490 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004491 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004492 if opts.diff:
4493 sys.stdout.write(stdout)
4494 else:
4495 # TODO(sbc): yapf --lines mode still has some issues.
4496 # https://github.com/google/yapf/issues/154
4497 DieWithError('--python currently only works with --full')
4498
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004499 # Dart's formatter does not have the nice property of only operating on
4500 # modified chunks, so hard code full.
4501 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004502 try:
4503 command = [dart_format.FindDartFmtToolInChromiumTree()]
4504 if not opts.dry_run and not opts.diff:
4505 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004506 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004507
ppi@chromium.org6593d932016-03-03 15:41:15 +00004508 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004509 if opts.dry_run and stdout:
4510 return_value = 2
4511 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004512 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4513 'found in this checkout. Files in other languages are still ' +
4514 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004515
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004516 # Format GN build files. Always run on full build files for canonical form.
4517 if gn_diff_files:
4518 cmd = ['gn', 'format']
4519 if not opts.dry_run and not opts.diff:
4520 cmd.append('--in-place')
4521 for gn_diff_file in gn_diff_files:
4522 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4523 if opts.diff:
4524 sys.stdout.write(stdout)
4525
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004526 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004527
4528
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004529@subcommand.usage('<codereview url or issue id>')
4530def CMDcheckout(parser, args):
4531 """Checks out a branch associated with a given Rietveld issue."""
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004532 # TODO(tandrii): consider adding this for Gerrit?
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004533 _, args = parser.parse_args(args)
4534
4535 if len(args) != 1:
4536 parser.print_help()
4537 return 1
4538
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004539 issue_arg = ParseIssueNumberArgument(args[0])
4540 if issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004541 parser.print_help()
4542 return 1
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004543 target_issue = issue_arg.issue
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004544
4545 key_and_issues = [x.split() for x in RunGit(
4546 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4547 .splitlines()]
4548 branches = []
4549 for key, issue in key_and_issues:
4550 if issue == target_issue:
4551 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4552
4553 if len(branches) == 0:
4554 print 'No branch found for issue %s.' % target_issue
4555 return 1
4556 if len(branches) == 1:
4557 RunGit(['checkout', branches[0]])
4558 else:
4559 print 'Multiple branches match issue %s:' % target_issue
4560 for i in range(len(branches)):
4561 print '%d: %s' % (i, branches[i])
4562 which = raw_input('Choose by index: ')
4563 try:
4564 RunGit(['checkout', branches[int(which)]])
4565 except (IndexError, ValueError):
4566 print 'Invalid selection, not checking out any branch.'
4567 return 1
4568
4569 return 0
4570
4571
maruel@chromium.org29404b52014-09-08 22:58:00 +00004572def CMDlol(parser, args):
4573 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004574 print zlib.decompress(base64.b64decode(
4575 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4576 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4577 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4578 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004579 return 0
4580
4581
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004582class OptionParser(optparse.OptionParser):
4583 """Creates the option parse and add --verbose support."""
4584 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004585 optparse.OptionParser.__init__(
4586 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004587 self.add_option(
4588 '-v', '--verbose', action='count', default=0,
4589 help='Use 2 times for more debugging info')
4590
4591 def parse_args(self, args=None, values=None):
4592 options, args = optparse.OptionParser.parse_args(self, args, values)
4593 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4594 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4595 return options, args
4596
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004597
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004598def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004599 if sys.hexversion < 0x02060000:
4600 print >> sys.stderr, (
4601 '\nYour python version %s is unsupported, please upgrade.\n' %
4602 sys.version.split(' ', 1)[0])
4603 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004604
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004605 # Reload settings.
4606 global settings
4607 settings = Settings()
4608
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004609 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004610 dispatcher = subcommand.CommandDispatcher(__name__)
4611 try:
4612 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004613 except auth.AuthenticationError as e:
4614 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004615 except urllib2.HTTPError, e:
4616 if e.code != 500:
4617 raise
4618 DieWithError(
4619 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4620 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004621 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004622
4623
4624if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004625 # These affect sys.stdout so do it outside of main() to simplify mocks in
4626 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004627 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004628 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004629 try:
4630 sys.exit(main(sys.argv[1:]))
4631 except KeyboardInterrupt:
4632 sys.stderr.write('interrupted\n')
4633 sys.exit(1)