blob: 4590e210899c994efeea85163fc4b025520803d8 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000838class _ParsedIssueNumberArgument(object):
839 def __init__(self, issue=None, patchset=None, hostname=None):
840 self.issue = issue
841 self.patchset = patchset
842 self.hostname = hostname
843
844 @property
845 def valid(self):
846 return self.issue is not None
847
848
849class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
850 def __init__(self, *args, **kwargs):
851 self.patch_url = kwargs.pop('patch_url', None)
852 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
853
854
855def ParseIssueNumberArgument(arg):
856 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
857 fail_result = _ParsedIssueNumberArgument()
858
859 if arg.isdigit():
860 return _ParsedIssueNumberArgument(issue=int(arg))
861 if not arg.startswith('http'):
862 return fail_result
863 url = gclient_utils.UpgradeToHttps(arg)
864 try:
865 parsed_url = urlparse.urlparse(url)
866 except ValueError:
867 return fail_result
868 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
869 tmp = cls.ParseIssueURL(parsed_url)
870 if tmp is not None:
871 return tmp
872 return fail_result
873
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000876 """Changelist works with one changelist in local branch.
877
878 Supports two codereview backends: Rietveld or Gerrit, selected at object
879 creation.
880
881 Not safe for concurrent multi-{thread,process} use.
882 """
883
884 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
885 """Create a new ChangeList instance.
886
887 If issue is given, the codereview must be given too.
888
889 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
890 Otherwise, it's decided based on current configuration of the local branch,
891 with default being 'rietveld' for backwards compatibility.
892 See _load_codereview_impl for more details.
893
894 **kwargs will be passed directly to codereview implementation.
895 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000897 global settings
898 if not settings:
899 # Happens when git_cl.py is used as a utility library.
900 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901
902 if issue:
903 assert codereview, 'codereview must be known, if issue is known'
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.branchref = branchref
906 if self.branchref:
907 self.branch = ShortBranchName(self.branchref)
908 else:
909 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000911 self.lookedup_issue = False
912 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.has_description = False
914 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000915 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000917 self.cc = None
918 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000922 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000924 assert self._codereview_impl
925 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 def _load_codereview_impl(self, codereview=None, **kwargs):
928 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000929 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
930 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
931 self._codereview = codereview
932 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 return
934
935 # Automatic selection based on issue number set for a current branch.
936 # Rietveld takes precedence over Gerrit.
937 assert not self.issue
938 # Whether we find issue or not, we are doing the lookup.
939 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000940 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 setting = cls.IssueSetting(self.GetBranch())
942 issue = RunGit(['config', setting], error_ok=True).strip()
943 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = cls(self, **kwargs)
946 self.issue = int(issue)
947 return
948
949 # No issue is set for this branch, so decide based on repo-wide settings.
950 return self._load_codereview_impl(
951 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
952 **kwargs)
953
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 def IsGerrit(self):
955 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000956
957 def GetCCList(self):
958 """Return the users cc'd on this CL.
959
960 Return is a string suitable for passing to gcl with the --cc flag.
961 """
962 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000963 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000964 more_cc = ','.join(self.watchers)
965 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
966 return self.cc
967
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000968 def GetCCListWithoutDefault(self):
969 """Return the users cc'd on this CL excluding default ones."""
970 if self.cc is None:
971 self.cc = ','.join(self.watchers)
972 return self.cc
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def SetWatchers(self, watchers):
975 """Set the list of email addresses that should be cc'd based on the changed
976 files in this CL.
977 """
978 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
980 def GetBranch(self):
981 """Returns the short branch name, e.g. 'master'."""
982 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000984 if not branchref:
985 return None
986 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 self.branch = ShortBranchName(self.branchref)
988 return self.branch
989
990 def GetBranchRef(self):
991 """Returns the full branch name, e.g. 'refs/heads/master'."""
992 self.GetBranch() # Poke the lazy loader.
993 return self.branchref
994
tandrii@chromium.org534f67a2016-04-07 18:47:05 +0000995 def ClearBranch(self):
996 """Clears cached branch data of this object."""
997 self.branch = self.branchref = None
998
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000999 @staticmethod
1000 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001001 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 e.g. 'origin', 'refs/heads/master'
1003 """
1004 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1006 error_ok=True).strip()
1007 if upstream_branch:
1008 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1009 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001010 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1011 error_ok=True).strip()
1012 if upstream_branch:
1013 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Fall back on trying a git-svn upstream branch.
1016 if settings.GetIsGitSvn():
1017 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001019 # Else, try to guess the origin remote.
1020 remote_branches = RunGit(['branch', '-r']).split()
1021 if 'origin/master' in remote_branches:
1022 # Fall back on origin/master if it exits.
1023 remote = 'origin'
1024 upstream_branch = 'refs/heads/master'
1025 elif 'origin/trunk' in remote_branches:
1026 # Fall back on origin/trunk if it exists. Generally a shared
1027 # git-svn clone
1028 remote = 'origin'
1029 upstream_branch = 'refs/heads/trunk'
1030 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001031 DieWithError(
1032 'Unable to determine default branch to diff against.\n'
1033 'Either pass complete "git diff"-style arguments, like\n'
1034 ' git cl upload origin/master\n'
1035 'or verify this branch is set up to track another \n'
1036 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
1038 return remote, upstream_branch
1039
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001040 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001041 upstream_branch = self.GetUpstreamBranch()
1042 if not BranchExists(upstream_branch):
1043 DieWithError('The upstream for the current branch (%s) does not exist '
1044 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001045 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001046 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 def GetUpstreamBranch(self):
1049 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001052 upstream_branch = upstream_branch.replace('refs/heads/',
1053 'refs/remotes/%s/' % remote)
1054 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1055 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 self.upstream_branch = upstream_branch
1057 return self.upstream_branch
1058
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001059 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001060 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 remote, branch = None, self.GetBranch()
1062 seen_branches = set()
1063 while branch not in seen_branches:
1064 seen_branches.add(branch)
1065 remote, branch = self.FetchUpstreamTuple(branch)
1066 branch = ShortBranchName(branch)
1067 if remote != '.' or branch.startswith('refs/remotes'):
1068 break
1069 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001070 remotes = RunGit(['remote'], error_ok=True).split()
1071 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001072 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001073 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001075 logging.warning('Could not determine which remote this change is '
1076 'associated with, so defaulting to "%s". This may '
1077 'not be what you want. You may prevent this message '
1078 'by running "git svn info" as documented here: %s',
1079 self._remote,
1080 GIT_INSTRUCTIONS_URL)
1081 else:
1082 logging.warn('Could not determine which remote this change is '
1083 'associated with. You may prevent this message by '
1084 'running "git svn info" as documented here: %s',
1085 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 branch = 'HEAD'
1087 if branch.startswith('refs/remotes'):
1088 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001089 elif branch.startswith('refs/branch-heads/'):
1090 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 else:
1092 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001093 return self._remote
1094
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001095 def GitSanityChecks(self, upstream_git_obj):
1096 """Checks git repo status and ensures diff is from local commits."""
1097
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 if upstream_git_obj is None:
1099 if self.GetBranch() is None:
1100 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001101 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001102 else:
1103 print >> sys.stderr, (
1104 'ERROR: no upstream branch')
1105 return False
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 # Verify the commit we're diffing against is in our current branch.
1108 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1109 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1110 if upstream_sha != common_ancestor:
1111 print >> sys.stderr, (
1112 'ERROR: %s is not in the current branch. You may need to rebase '
1113 'your tracking branch' % upstream_sha)
1114 return False
1115
1116 # List the commits inside the diff, and verify they are all local.
1117 commits_in_diff = RunGit(
1118 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1119 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1120 remote_branch = remote_branch.strip()
1121 if code != 0:
1122 _, remote_branch = self.GetRemoteBranch()
1123
1124 commits_in_remote = RunGit(
1125 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1126
1127 common_commits = set(commits_in_diff) & set(commits_in_remote)
1128 if common_commits:
1129 print >> sys.stderr, (
1130 'ERROR: Your diff contains %d commits already in %s.\n'
1131 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1132 'the diff. If you are using a custom git flow, you can override'
1133 ' the reference used for this check with "git config '
1134 'gitcl.remotebranch <git-ref>".' % (
1135 len(common_commits), remote_branch, upstream_git_obj))
1136 return False
1137 return True
1138
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001139 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001141
1142 Returns None if it is not set.
1143 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001144 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1145 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001146
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001147 def GetGitSvnRemoteUrl(self):
1148 """Return the configured git-svn remote URL parsed from git svn info.
1149
1150 Returns None if it is not set.
1151 """
1152 # URL is dependent on the current directory.
1153 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1154 if data:
1155 keys = dict(line.split(': ', 1) for line in data.splitlines()
1156 if ': ' in line)
1157 return keys.get('URL', None)
1158 return None
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 def GetRemoteUrl(self):
1161 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1162
1163 Returns None if there is no remote.
1164 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001165 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001166 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1167
1168 # If URL is pointing to a local directory, it is probably a git cache.
1169 if os.path.isdir(url):
1170 url = RunGit(['config', 'remote.%s.url' % remote],
1171 error_ok=True,
1172 cwd=url).strip()
1173 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001175 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001176 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001177 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 issue = RunGit(['config',
1179 self._codereview_impl.IssueSetting(self.GetBranch())],
1180 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001181 self.issue = int(issue) or None if issue else None
1182 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 return self.issue
1184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 def GetIssueURL(self):
1186 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001187 issue = self.GetIssue()
1188 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001189 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
1192 def GetDescription(self, pretty=False):
1193 if not self.has_description:
1194 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 self.has_description = True
1197 if pretty:
1198 wrapper = textwrap.TextWrapper()
1199 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1200 return wrapper.fill(self.description)
1201 return self.description
1202
1203 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001204 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 self.patchset = int(patchset) or None if patchset else None
1209 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return self.patchset
1211
1212 def SetPatchset(self, patchset):
1213 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001216 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001220 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001221 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001223 def SetIssue(self, issue=None):
1224 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1226 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001228 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 RunGit(['config', issue_setting, str(issue)])
1230 codereview_server = self._codereview_impl.GetCodereviewServer()
1231 if codereview_server:
1232 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001234 current_issue = self.GetIssue()
1235 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001237 self.issue = None
1238 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001240 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001241 if not self.GitSanityChecks(upstream_branch):
1242 DieWithError('\nGit sanity check failure')
1243
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001244 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001245 if not root:
1246 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001248
1249 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001250 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001251 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001252 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001253 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001254 except subprocess2.CalledProcessError:
1255 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001256 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001257 'This branch probably doesn\'t exist anymore. To reset the\n'
1258 'tracking branch, please run\n'
1259 ' git branch --set-upstream %s trunk\n'
1260 'replacing trunk with origin/master or the relevant branch') %
1261 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001262
maruel@chromium.org52424302012-08-29 15:14:30 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001265 if issue:
1266 description = self.GetDescription()
1267 else:
1268 # If the change was never uploaded, use the log messages of all commits
1269 # up to the branch point, as git cl upload will prefill the description
1270 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001271 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1272 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001273
1274 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001275 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001276 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001277 name,
1278 description,
1279 absroot,
1280 files,
1281 issue,
1282 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001283 author,
1284 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001285
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self._codereview_impl.UpdateDescriptionRemote(description)
1289
1290 def RunHook(self, committing, may_prompt, verbose, change):
1291 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1292 try:
1293 return presubmit_support.DoPresubmitChecks(change, committing,
1294 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1295 default_presubmit=None, may_prompt=may_prompt,
1296 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1297 except presubmit_support.PresubmitFailure, e:
1298 DieWithError(
1299 ('%s\nMaybe your depot_tools is out of date?\n'
1300 'If all fails, contact maruel@') % e)
1301
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001302 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1303 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001304 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1305 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001306 else:
1307 # Assume url.
1308 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1309 urlparse.urlparse(issue_arg))
1310 if not parsed_issue_arg or not parsed_issue_arg.valid:
1311 DieWithError('Failed to parse issue argument "%s". '
1312 'Must be an issue number or a valid URL.' % issue_arg)
1313 return self._codereview_impl.CMDPatchWithParsedIssue(
1314 parsed_issue_arg, reject, nocommit, directory)
1315
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001316 # Forward methods to codereview specific implementation.
1317
1318 def CloseIssue(self):
1319 return self._codereview_impl.CloseIssue()
1320
1321 def GetStatus(self):
1322 return self._codereview_impl.GetStatus()
1323
1324 def GetCodereviewServer(self):
1325 return self._codereview_impl.GetCodereviewServer()
1326
1327 def GetApprovingReviewers(self):
1328 return self._codereview_impl.GetApprovingReviewers()
1329
1330 def GetMostRecentPatchset(self):
1331 return self._codereview_impl.GetMostRecentPatchset()
1332
1333 def __getattr__(self, attr):
1334 # This is because lots of untested code accesses Rietveld-specific stuff
1335 # directly, and it's hard to fix for sure. So, just let it work, and fix
1336 # on a cases by case basis.
1337 return getattr(self._codereview_impl, attr)
1338
1339
1340class _ChangelistCodereviewBase(object):
1341 """Abstract base class encapsulating codereview specifics of a changelist."""
1342 def __init__(self, changelist):
1343 self._changelist = changelist # instance of Changelist
1344
1345 def __getattr__(self, attr):
1346 # Forward methods to changelist.
1347 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1348 # _RietveldChangelistImpl to avoid this hack?
1349 return getattr(self._changelist, attr)
1350
1351 def GetStatus(self):
1352 """Apply a rough heuristic to give a simple summary of an issue's review
1353 or CQ status, assuming adherence to a common workflow.
1354
1355 Returns None if no issue for this branch, or specific string keywords.
1356 """
1357 raise NotImplementedError()
1358
1359 def GetCodereviewServer(self):
1360 """Returns server URL without end slash, like "https://codereview.com"."""
1361 raise NotImplementedError()
1362
1363 def FetchDescription(self):
1364 """Fetches and returns description from the codereview server."""
1365 raise NotImplementedError()
1366
1367 def GetCodereviewServerSetting(self):
1368 """Returns git config setting for the codereview server."""
1369 raise NotImplementedError()
1370
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001371 @classmethod
1372 def IssueSetting(cls, branch):
1373 return 'branch.%s.%s' % (branch, cls.IssueSettingPrefix())
1374
1375 @classmethod
1376 def IssueSettingPrefix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001377 """Returns name of git config setting which stores issue number for a given
1378 branch."""
1379 raise NotImplementedError()
1380
1381 def PatchsetSetting(self):
1382 """Returns name of git config setting which stores issue number."""
1383 raise NotImplementedError()
1384
1385 def GetRieveldObjForPresubmit(self):
1386 # This is an unfortunate Rietveld-embeddedness in presubmit.
1387 # For non-Rietveld codereviews, this probably should return a dummy object.
1388 raise NotImplementedError()
1389
1390 def UpdateDescriptionRemote(self, description):
1391 """Update the description on codereview site."""
1392 raise NotImplementedError()
1393
1394 def CloseIssue(self):
1395 """Closes the issue."""
1396 raise NotImplementedError()
1397
1398 def GetApprovingReviewers(self):
1399 """Returns a list of reviewers approving the change.
1400
1401 Note: not necessarily committers.
1402 """
1403 raise NotImplementedError()
1404
1405 def GetMostRecentPatchset(self):
1406 """Returns the most recent patchset number from the codereview site."""
1407 raise NotImplementedError()
1408
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001409 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1410 directory):
1411 """Fetches and applies the issue.
1412
1413 Arguments:
1414 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1415 reject: if True, reject the failed patch instead of switching to 3-way
1416 merge. Rietveld only.
1417 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1418 only.
1419 directory: switch to directory before applying the patch. Rietveld only.
1420 """
1421 raise NotImplementedError()
1422
1423 @staticmethod
1424 def ParseIssueURL(parsed_url):
1425 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1426 failed."""
1427 raise NotImplementedError()
1428
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001429
1430class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1431 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1432 super(_RietveldChangelistImpl, self).__init__(changelist)
1433 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1434 settings.GetDefaultServerUrl()
1435
1436 self._rietveld_server = rietveld_server
1437 self._auth_config = auth_config
1438 self._props = None
1439 self._rpc_server = None
1440
1441 def GetAuthConfig(self):
1442 return self._auth_config
1443
1444 def GetCodereviewServer(self):
1445 if not self._rietveld_server:
1446 # If we're on a branch then get the server potentially associated
1447 # with that branch.
1448 if self.GetIssue():
1449 rietveld_server_setting = self.GetCodereviewServerSetting()
1450 if rietveld_server_setting:
1451 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1452 ['config', rietveld_server_setting], error_ok=True).strip())
1453 if not self._rietveld_server:
1454 self._rietveld_server = settings.GetDefaultServerUrl()
1455 return self._rietveld_server
1456
1457 def FetchDescription(self):
1458 issue = self.GetIssue()
1459 assert issue
1460 try:
1461 return self.RpcServer().get_description(issue).strip()
1462 except urllib2.HTTPError as e:
1463 if e.code == 404:
1464 DieWithError(
1465 ('\nWhile fetching the description for issue %d, received a '
1466 '404 (not found)\n'
1467 'error. It is likely that you deleted this '
1468 'issue on the server. If this is the\n'
1469 'case, please run\n\n'
1470 ' git cl issue 0\n\n'
1471 'to clear the association with the deleted issue. Then run '
1472 'this command again.') % issue)
1473 else:
1474 DieWithError(
1475 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1476 except urllib2.URLError as e:
1477 print >> sys.stderr, (
1478 'Warning: Failed to retrieve CL description due to network '
1479 'failure.')
1480 return ''
1481
1482 def GetMostRecentPatchset(self):
1483 return self.GetIssueProperties()['patchsets'][-1]
1484
1485 def GetPatchSetDiff(self, issue, patchset):
1486 return self.RpcServer().get(
1487 '/download/issue%s_%s.diff' % (issue, patchset))
1488
1489 def GetIssueProperties(self):
1490 if self._props is None:
1491 issue = self.GetIssue()
1492 if not issue:
1493 self._props = {}
1494 else:
1495 self._props = self.RpcServer().get_issue_properties(issue, True)
1496 return self._props
1497
1498 def GetApprovingReviewers(self):
1499 return get_approving_reviewers(self.GetIssueProperties())
1500
1501 def AddComment(self, message):
1502 return self.RpcServer().add_comment(self.GetIssue(), message)
1503
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001504 def GetStatus(self):
1505 """Apply a rough heuristic to give a simple summary of an issue's review
1506 or CQ status, assuming adherence to a common workflow.
1507
1508 Returns None if no issue for this branch, or one of the following keywords:
1509 * 'error' - error from review tool (including deleted issues)
1510 * 'unsent' - not sent for review
1511 * 'waiting' - waiting for review
1512 * 'reply' - waiting for owner to reply to review
1513 * 'lgtm' - LGTM from at least one approved reviewer
1514 * 'commit' - in the commit queue
1515 * 'closed' - closed
1516 """
1517 if not self.GetIssue():
1518 return None
1519
1520 try:
1521 props = self.GetIssueProperties()
1522 except urllib2.HTTPError:
1523 return 'error'
1524
1525 if props.get('closed'):
1526 # Issue is closed.
1527 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001528 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001529 # Issue is in the commit queue.
1530 return 'commit'
1531
1532 try:
1533 reviewers = self.GetApprovingReviewers()
1534 except urllib2.HTTPError:
1535 return 'error'
1536
1537 if reviewers:
1538 # Was LGTM'ed.
1539 return 'lgtm'
1540
1541 messages = props.get('messages') or []
1542
1543 if not messages:
1544 # No message was sent.
1545 return 'unsent'
1546 if messages[-1]['sender'] != props.get('owner_email'):
1547 # Non-LGTM reply from non-owner
1548 return 'reply'
1549 return 'waiting'
1550
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001551 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001552 return self.RpcServer().update_description(
1553 self.GetIssue(), self.description)
1554
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001555 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001556 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001558 def SetFlag(self, flag, value):
1559 """Patchset must match."""
1560 if not self.GetPatchset():
1561 DieWithError('The patchset needs to match. Send another patchset.')
1562 try:
1563 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001564 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001565 except urllib2.HTTPError, e:
1566 if e.code == 404:
1567 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1568 if e.code == 403:
1569 DieWithError(
1570 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1571 'match?') % (self.GetIssue(), self.GetPatchset()))
1572 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001573
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001574 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575 """Returns an upload.RpcServer() to access this review's rietveld instance.
1576 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001577 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001578 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001580 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001581 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001583 @classmethod
1584 def IssueSettingPrefix(cls):
1585 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001586
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001587 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588 """Return the git setting that stores this change's most recent patchset."""
1589 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1590
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001591 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001592 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001593 branch = self.GetBranch()
1594 if branch:
1595 return 'branch.%s.rietveldserver' % branch
1596 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001598 def GetRieveldObjForPresubmit(self):
1599 return self.RpcServer()
1600
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001601 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1602 directory):
1603 # TODO(maruel): Use apply_issue.py
1604
1605 # PatchIssue should never be called with a dirty tree. It is up to the
1606 # caller to check this, but just in case we assert here since the
1607 # consequences of the caller not checking this could be dire.
1608 assert(not git_common.is_dirty_git_tree('apply'))
1609 assert(parsed_issue_arg.valid)
1610 self._changelist.issue = parsed_issue_arg.issue
1611 if parsed_issue_arg.hostname:
1612 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1613
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001614 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1615 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001616 assert parsed_issue_arg.patchset
1617 patchset = parsed_issue_arg.patchset
1618 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1619 else:
1620 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1621 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1622
1623 # Switch up to the top-level directory, if necessary, in preparation for
1624 # applying the patch.
1625 top = settings.GetRelativeRoot()
1626 if top:
1627 os.chdir(top)
1628
1629 # Git patches have a/ at the beginning of source paths. We strip that out
1630 # with a sed script rather than the -p flag to patch so we can feed either
1631 # Git or svn-style patches into the same apply command.
1632 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1633 try:
1634 patch_data = subprocess2.check_output(
1635 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1636 except subprocess2.CalledProcessError:
1637 DieWithError('Git patch mungling failed.')
1638 logging.info(patch_data)
1639
1640 # We use "git apply" to apply the patch instead of "patch" so that we can
1641 # pick up file adds.
1642 # The --index flag means: also insert into the index (so we catch adds).
1643 cmd = ['git', 'apply', '--index', '-p0']
1644 if directory:
1645 cmd.extend(('--directory', directory))
1646 if reject:
1647 cmd.append('--reject')
1648 elif IsGitVersionAtLeast('1.7.12'):
1649 cmd.append('--3way')
1650 try:
1651 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1652 stdin=patch_data, stdout=subprocess2.VOID)
1653 except subprocess2.CalledProcessError:
1654 print 'Failed to apply the patch'
1655 return 1
1656
1657 # If we had an issue, commit the current state and register the issue.
1658 if not nocommit:
1659 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1660 'patch from issue %(i)s at patchset '
1661 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1662 % {'i': self.GetIssue(), 'p': patchset})])
1663 self.SetIssue(self.GetIssue())
1664 self.SetPatchset(patchset)
1665 print "Committed patch locally."
1666 else:
1667 print "Patch applied to index."
1668 return 0
1669
1670 @staticmethod
1671 def ParseIssueURL(parsed_url):
1672 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1673 return None
1674 # Typical url: https://domain/<issue_number>[/[other]]
1675 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1676 if match:
1677 return _RietveldParsedIssueNumberArgument(
1678 issue=int(match.group(1)),
1679 hostname=parsed_url.netloc)
1680 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1681 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1682 if match:
1683 return _RietveldParsedIssueNumberArgument(
1684 issue=int(match.group(1)),
1685 patchset=int(match.group(2)),
1686 hostname=parsed_url.netloc,
1687 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1688 return None
1689
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001690
1691class _GerritChangelistImpl(_ChangelistCodereviewBase):
1692 def __init__(self, changelist, auth_config=None):
1693 # auth_config is Rietveld thing, kept here to preserve interface only.
1694 super(_GerritChangelistImpl, self).__init__(changelist)
1695 self._change_id = None
1696 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1697 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1698
1699 def _GetGerritHost(self):
1700 # Lazy load of configs.
1701 self.GetCodereviewServer()
1702 return self._gerrit_host
1703
1704 def GetCodereviewServer(self):
1705 if not self._gerrit_server:
1706 # If we're on a branch then get the server potentially associated
1707 # with that branch.
1708 if self.GetIssue():
1709 gerrit_server_setting = self.GetCodereviewServerSetting()
1710 if gerrit_server_setting:
1711 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1712 error_ok=True).strip()
1713 if self._gerrit_server:
1714 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1715 if not self._gerrit_server:
1716 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1717 # has "-review" suffix for lowest level subdomain.
1718 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1719 parts[0] = parts[0] + '-review'
1720 self._gerrit_host = '.'.join(parts)
1721 self._gerrit_server = 'https://%s' % self._gerrit_host
1722 return self._gerrit_server
1723
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001724 @classmethod
1725 def IssueSettingPrefix(cls):
1726 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001727
1728 def PatchsetSetting(self):
1729 """Return the git setting that stores this change's most recent patchset."""
1730 return 'branch.%s.gerritpatchset' % self.GetBranch()
1731
1732 def GetCodereviewServerSetting(self):
1733 """Returns the git setting that stores this change's Gerrit server."""
1734 branch = self.GetBranch()
1735 if branch:
1736 return 'branch.%s.gerritserver' % branch
1737 return None
1738
1739 def GetRieveldObjForPresubmit(self):
1740 class ThisIsNotRietveldIssue(object):
1741 def __nonzero__(self):
1742 # This is a hack to make presubmit_support think that rietveld is not
1743 # defined, yet still ensure that calls directly result in a decent
1744 # exception message below.
1745 return False
1746
1747 def __getattr__(self, attr):
1748 print(
1749 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1750 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1751 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1752 'or use Rietveld for codereview.\n'
1753 'See also http://crbug.com/579160.' % attr)
1754 raise NotImplementedError()
1755 return ThisIsNotRietveldIssue()
1756
1757 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001758 """Apply a rough heuristic to give a simple summary of an issue's review
1759 or CQ status, assuming adherence to a common workflow.
1760
1761 Returns None if no issue for this branch, or one of the following keywords:
1762 * 'error' - error from review tool (including deleted issues)
1763 * 'unsent' - no reviewers added
1764 * 'waiting' - waiting for review
1765 * 'reply' - waiting for owner to reply to review
1766 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1767 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1768 * 'commit' - in the commit queue
1769 * 'closed' - abandoned
1770 """
1771 if not self.GetIssue():
1772 return None
1773
1774 try:
1775 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1776 except httplib.HTTPException:
1777 return 'error'
1778
1779 if data['status'] == 'ABANDONED':
1780 return 'closed'
1781
1782 cq_label = data['labels'].get('Commit-Queue', {})
1783 if cq_label:
1784 # Vote value is a stringified integer, which we expect from 0 to 2.
1785 vote_value = cq_label.get('value', '0')
1786 vote_text = cq_label.get('values', {}).get(vote_value, '')
1787 if vote_text.lower() == 'commit':
1788 return 'commit'
1789
1790 lgtm_label = data['labels'].get('Code-Review', {})
1791 if lgtm_label:
1792 if 'rejected' in lgtm_label:
1793 return 'not lgtm'
1794 if 'approved' in lgtm_label:
1795 return 'lgtm'
1796
1797 if not data.get('reviewers', {}).get('REVIEWER', []):
1798 return 'unsent'
1799
1800 messages = data.get('messages', [])
1801 if messages:
1802 owner = data['owner'].get('_account_id')
1803 last_message_author = messages[-1].get('author', {}).get('_account_id')
1804 if owner != last_message_author:
1805 # Some reply from non-owner.
1806 return 'reply'
1807
1808 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001809
1810 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001811 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001812 return data['revisions'][data['current_revision']]['_number']
1813
1814 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001815 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001816 return data['revisions'][data['current_revision']]['commit_with_footers']
1817
1818 def UpdateDescriptionRemote(self, description):
1819 # TODO(tandrii)
1820 raise NotImplementedError()
1821
1822 def CloseIssue(self):
1823 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1824
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001825 def SubmitIssue(self, wait_for_merge=True):
1826 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
1827 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001829 def _GetChangeDetail(self, options):
1830 return gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1831 options)
1832
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001833 def CMDLand(self, force, bypass_hooks, verbose):
1834 if git_common.is_dirty_git_tree('land'):
1835 return 1
1836 differs = True
1837 last_upload = RunGit(['config',
1838 'branch.%s.gerritsquashhash' % self.GetBranch()],
1839 error_ok=True).strip()
1840 # Note: git diff outputs nothing if there is no diff.
1841 if not last_upload or RunGit(['diff', last_upload]).strip():
1842 print('WARNING: some changes from local branch haven\'t been uploaded')
1843 else:
1844 detail = self._GetChangeDetail(['CURRENT_REVISION'])
1845 if detail['current_revision'] == last_upload:
1846 differs = False
1847 else:
1848 print('WARNING: local branch contents differ from latest uploaded '
1849 'patchset')
1850 if differs:
1851 if not force:
1852 ask_for_data(
1853 'Do you want to submit latest Gerrit patchset and bypass hooks?')
1854 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
1855 elif not bypass_hooks:
1856 hook_results = self.RunHook(
1857 committing=True,
1858 may_prompt=not force,
1859 verbose=verbose,
1860 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
1861 if not hook_results.should_continue():
1862 return 1
1863
1864 self.SubmitIssue(wait_for_merge=True)
1865 print('Issue %s has been submitted.' % self.GetIssueURL())
1866 return 0
1867
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001868 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1869 directory):
1870 assert not reject
1871 assert not nocommit
1872 assert not directory
1873 assert parsed_issue_arg.valid
1874
1875 self._changelist.issue = parsed_issue_arg.issue
1876
1877 if parsed_issue_arg.hostname:
1878 self._gerrit_host = parsed_issue_arg.hostname
1879 self._gerrit_server = 'https://%s' % self._gerrit_host
1880
1881 detail = self._GetChangeDetail(['ALL_REVISIONS'])
1882
1883 if not parsed_issue_arg.patchset:
1884 # Use current revision by default.
1885 revision_info = detail['revisions'][detail['current_revision']]
1886 patchset = int(revision_info['_number'])
1887 else:
1888 patchset = parsed_issue_arg.patchset
1889 for revision_info in detail['revisions'].itervalues():
1890 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1891 break
1892 else:
1893 DieWithError('Couldn\'t find patchset %i in issue %i' %
1894 (parsed_issue_arg.patchset, self.GetIssue()))
1895
1896 fetch_info = revision_info['fetch']['http']
1897 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
1898 RunGit(['cherry-pick', 'FETCH_HEAD'])
1899 self.SetIssue(self.GetIssue())
1900 self.SetPatchset(patchset)
1901 print('Committed patch for issue %i pathset %i locally' %
1902 (self.GetIssue(), self.GetPatchset()))
1903 return 0
1904
1905 @staticmethod
1906 def ParseIssueURL(parsed_url):
1907 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1908 return None
1909 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
1910 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
1911 # Short urls like https://domain/<issue_number> can be used, but don't allow
1912 # specifying the patchset (you'd 404), but we allow that here.
1913 if parsed_url.path == '/':
1914 part = parsed_url.fragment
1915 else:
1916 part = parsed_url.path
1917 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
1918 if match:
1919 return _ParsedIssueNumberArgument(
1920 issue=int(match.group(2)),
1921 patchset=int(match.group(4)) if match.group(4) else None,
1922 hostname=parsed_url.netloc)
1923 return None
1924
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001925
1926_CODEREVIEW_IMPLEMENTATIONS = {
1927 'rietveld': _RietveldChangelistImpl,
1928 'gerrit': _GerritChangelistImpl,
1929}
1930
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001931
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001932class ChangeDescription(object):
1933 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001934 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001935 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001936
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001937 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001938 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001939
agable@chromium.org42c20792013-09-12 17:34:49 +00001940 @property # www.logilab.org/ticket/89786
1941 def description(self): # pylint: disable=E0202
1942 return '\n'.join(self._description_lines)
1943
1944 def set_description(self, desc):
1945 if isinstance(desc, basestring):
1946 lines = desc.splitlines()
1947 else:
1948 lines = [line.rstrip() for line in desc]
1949 while lines and not lines[0]:
1950 lines.pop(0)
1951 while lines and not lines[-1]:
1952 lines.pop(-1)
1953 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001954
piman@chromium.org336f9122014-09-04 02:16:55 +00001955 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001956 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001957 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001958 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001959 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001960 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001961
agable@chromium.org42c20792013-09-12 17:34:49 +00001962 # Get the set of R= and TBR= lines and remove them from the desciption.
1963 regexp = re.compile(self.R_LINE)
1964 matches = [regexp.match(line) for line in self._description_lines]
1965 new_desc = [l for i, l in enumerate(self._description_lines)
1966 if not matches[i]]
1967 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001968
agable@chromium.org42c20792013-09-12 17:34:49 +00001969 # Construct new unified R= and TBR= lines.
1970 r_names = []
1971 tbr_names = []
1972 for match in matches:
1973 if not match:
1974 continue
1975 people = cleanup_list([match.group(2).strip()])
1976 if match.group(1) == 'TBR':
1977 tbr_names.extend(people)
1978 else:
1979 r_names.extend(people)
1980 for name in r_names:
1981 if name not in reviewers:
1982 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001983 if add_owners_tbr:
1984 owners_db = owners.Database(change.RepositoryRoot(),
1985 fopen=file, os_path=os.path, glob=glob.glob)
1986 all_reviewers = set(tbr_names + reviewers)
1987 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1988 all_reviewers)
1989 tbr_names.extend(owners_db.reviewers_for(missing_files,
1990 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001991 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1992 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1993
1994 # Put the new lines in the description where the old first R= line was.
1995 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1996 if 0 <= line_loc < len(self._description_lines):
1997 if new_tbr_line:
1998 self._description_lines.insert(line_loc, new_tbr_line)
1999 if new_r_line:
2000 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002001 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002002 if new_r_line:
2003 self.append_footer(new_r_line)
2004 if new_tbr_line:
2005 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002006
2007 def prompt(self):
2008 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002009 self.set_description([
2010 '# Enter a description of the change.',
2011 '# This will be displayed on the codereview site.',
2012 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002013 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002014 '--------------------',
2015 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002016
agable@chromium.org42c20792013-09-12 17:34:49 +00002017 regexp = re.compile(self.BUG_LINE)
2018 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002019 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002020 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002021 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002022 if not content:
2023 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002024 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002025
2026 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002027 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2028 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002029 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002030 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002031
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002032 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002033 if self._description_lines:
2034 # Add an empty line if either the last line or the new line isn't a tag.
2035 last_line = self._description_lines[-1]
2036 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2037 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2038 self._description_lines.append('')
2039 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002040
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002041 def get_reviewers(self):
2042 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002043 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2044 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002045 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002046
2047
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002048def get_approving_reviewers(props):
2049 """Retrieves the reviewers that approved a CL from the issue properties with
2050 messages.
2051
2052 Note that the list may contain reviewers that are not committer, thus are not
2053 considered by the CQ.
2054 """
2055 return sorted(
2056 set(
2057 message['sender']
2058 for message in props['messages']
2059 if message['approval'] and message['sender'] in props['reviewers']
2060 )
2061 )
2062
2063
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064def FindCodereviewSettingsFile(filename='codereview.settings'):
2065 """Finds the given file starting in the cwd and going up.
2066
2067 Only looks up to the top of the repository unless an
2068 'inherit-review-settings-ok' file exists in the root of the repository.
2069 """
2070 inherit_ok_file = 'inherit-review-settings-ok'
2071 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002072 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002073 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2074 root = '/'
2075 while True:
2076 if filename in os.listdir(cwd):
2077 if os.path.isfile(os.path.join(cwd, filename)):
2078 return open(os.path.join(cwd, filename))
2079 if cwd == root:
2080 break
2081 cwd = os.path.dirname(cwd)
2082
2083
2084def LoadCodereviewSettingsFromFile(fileobj):
2085 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002086 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002087
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002088 def SetProperty(name, setting, unset_error_ok=False):
2089 fullname = 'rietveld.' + name
2090 if setting in keyvals:
2091 RunGit(['config', fullname, keyvals[setting]])
2092 else:
2093 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2094
2095 SetProperty('server', 'CODE_REVIEW_SERVER')
2096 # Only server setting is required. Other settings can be absent.
2097 # In that case, we ignore errors raised during option deletion attempt.
2098 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002099 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002100 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2101 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002102 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002103 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002104 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2105 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002106 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002107 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002108 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002109 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2110 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002112 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002113 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002114
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002115 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2116 RunGit(['config', 'gerrit.squash-uploads',
2117 keyvals['GERRIT_SQUASH_UPLOADS']])
2118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2120 #should be of the form
2121 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2122 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2123 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2124 keyvals['ORIGIN_URL_CONFIG']])
2125
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002127def urlretrieve(source, destination):
2128 """urllib is broken for SSL connections via a proxy therefore we
2129 can't use urllib.urlretrieve()."""
2130 with open(destination, 'w') as f:
2131 f.write(urllib2.urlopen(source).read())
2132
2133
ukai@chromium.org712d6102013-11-27 00:52:58 +00002134def hasSheBang(fname):
2135 """Checks fname is a #! script."""
2136 with open(fname) as f:
2137 return f.read(2).startswith('#!')
2138
2139
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002140# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2141def DownloadHooks(*args, **kwargs):
2142 pass
2143
2144
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002145def DownloadGerritHook(force):
2146 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002147
2148 Args:
2149 force: True to update hooks. False to install hooks if not present.
2150 """
2151 if not settings.GetIsGerrit():
2152 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002153 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002154 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2155 if not os.access(dst, os.X_OK):
2156 if os.path.exists(dst):
2157 if not force:
2158 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002159 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002160 print(
2161 'WARNING: installing Gerrit commit-msg hook.\n'
2162 ' This behavior of git cl will soon be disabled.\n'
2163 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002164 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002165 if not hasSheBang(dst):
2166 DieWithError('Not a script: %s\n'
2167 'You need to download from\n%s\n'
2168 'into .git/hooks/commit-msg and '
2169 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002170 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2171 except Exception:
2172 if os.path.exists(dst):
2173 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002174 DieWithError('\nFailed to download hooks.\n'
2175 'You need to download from\n%s\n'
2176 'into .git/hooks/commit-msg and '
2177 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002178
2179
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002180
2181def GetRietveldCodereviewSettingsInteractively():
2182 """Prompt the user for settings."""
2183 server = settings.GetDefaultServerUrl(error_ok=True)
2184 prompt = 'Rietveld server (host[:port])'
2185 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2186 newserver = ask_for_data(prompt + ':')
2187 if not server and not newserver:
2188 newserver = DEFAULT_SERVER
2189 if newserver:
2190 newserver = gclient_utils.UpgradeToHttps(newserver)
2191 if newserver != server:
2192 RunGit(['config', 'rietveld.server', newserver])
2193
2194 def SetProperty(initial, caption, name, is_url):
2195 prompt = caption
2196 if initial:
2197 prompt += ' ("x" to clear) [%s]' % initial
2198 new_val = ask_for_data(prompt + ':')
2199 if new_val == 'x':
2200 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2201 elif new_val:
2202 if is_url:
2203 new_val = gclient_utils.UpgradeToHttps(new_val)
2204 if new_val != initial:
2205 RunGit(['config', 'rietveld.' + name, new_val])
2206
2207 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2208 SetProperty(settings.GetDefaultPrivateFlag(),
2209 'Private flag (rietveld only)', 'private', False)
2210 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2211 'tree-status-url', False)
2212 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2213 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2214 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2215 'run-post-upload-hook', False)
2216
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002217@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002218def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002219 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002220
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002221 print('WARNING: git cl config works for Rietveld only.\n'
2222 'For Gerrit, see http://crbug.com/579160.')
2223 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002224 parser.add_option('--activate-update', action='store_true',
2225 help='activate auto-updating [rietveld] section in '
2226 '.git/config')
2227 parser.add_option('--deactivate-update', action='store_true',
2228 help='deactivate auto-updating [rietveld] section in '
2229 '.git/config')
2230 options, args = parser.parse_args(args)
2231
2232 if options.deactivate_update:
2233 RunGit(['config', 'rietveld.autoupdate', 'false'])
2234 return
2235
2236 if options.activate_update:
2237 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2238 return
2239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002240 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002241 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002242 return 0
2243
2244 url = args[0]
2245 if not url.endswith('codereview.settings'):
2246 url = os.path.join(url, 'codereview.settings')
2247
2248 # Load code review settings and download hooks (if available).
2249 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2250 return 0
2251
2252
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002253def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002254 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002255 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2256 branch = ShortBranchName(branchref)
2257 _, args = parser.parse_args(args)
2258 if not args:
2259 print("Current base-url:")
2260 return RunGit(['config', 'branch.%s.base-url' % branch],
2261 error_ok=False).strip()
2262 else:
2263 print("Setting base-url to %s" % args[0])
2264 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2265 error_ok=False).strip()
2266
2267
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002268def color_for_status(status):
2269 """Maps a Changelist status to color, for CMDstatus and other tools."""
2270 return {
2271 'unsent': Fore.RED,
2272 'waiting': Fore.BLUE,
2273 'reply': Fore.YELLOW,
2274 'lgtm': Fore.GREEN,
2275 'commit': Fore.MAGENTA,
2276 'closed': Fore.CYAN,
2277 'error': Fore.WHITE,
2278 }.get(status, Fore.WHITE)
2279
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002280def fetch_cl_status(branch, auth_config=None):
2281 """Fetches information for an issue and returns (branch, issue, status)."""
2282 cl = Changelist(branchref=branch, auth_config=auth_config)
2283 url = cl.GetIssueURL()
2284 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002285
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002286 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002287 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002288 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002289
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002290 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002291
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002292def get_cl_statuses(
2293 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002294 """Returns a blocking iterable of (branch, issue, color) for given branches.
2295
2296 If fine_grained is true, this will fetch CL statuses from the server.
2297 Otherwise, simply indicate if there's a matching url for the given branches.
2298
2299 If max_processes is specified, it is used as the maximum number of processes
2300 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2301 spawned.
2302 """
2303 # Silence upload.py otherwise it becomes unwieldly.
2304 upload.verbosity = 0
2305
2306 if fine_grained:
2307 # Process one branch synchronously to work through authentication, then
2308 # spawn processes to process all the other branches in parallel.
2309 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002310 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2311 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002312
2313 branches_to_fetch = branches[1:]
2314 pool = ThreadPool(
2315 min(max_processes, len(branches_to_fetch))
2316 if max_processes is not None
2317 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002318 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002319 yield x
2320 else:
2321 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2322 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002323 cl = Changelist(branchref=b, auth_config=auth_config)
2324 url = cl.GetIssueURL()
2325 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002326
rmistry@google.com2dd99862015-06-22 12:22:18 +00002327
2328def upload_branch_deps(cl, args):
2329 """Uploads CLs of local branches that are dependents of the current branch.
2330
2331 If the local branch dependency tree looks like:
2332 test1 -> test2.1 -> test3.1
2333 -> test3.2
2334 -> test2.2 -> test3.3
2335
2336 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2337 run on the dependent branches in this order:
2338 test2.1, test3.1, test3.2, test2.2, test3.3
2339
2340 Note: This function does not rebase your local dependent branches. Use it when
2341 you make a change to the parent branch that will not conflict with its
2342 dependent branches, and you would like their dependencies updated in
2343 Rietveld.
2344 """
2345 if git_common.is_dirty_git_tree('upload-branch-deps'):
2346 return 1
2347
2348 root_branch = cl.GetBranch()
2349 if root_branch is None:
2350 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2351 'Get on a branch!')
2352 if not cl.GetIssue() or not cl.GetPatchset():
2353 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2354 'patchset dependencies without an uploaded CL.')
2355
2356 branches = RunGit(['for-each-ref',
2357 '--format=%(refname:short) %(upstream:short)',
2358 'refs/heads'])
2359 if not branches:
2360 print('No local branches found.')
2361 return 0
2362
2363 # Create a dictionary of all local branches to the branches that are dependent
2364 # on it.
2365 tracked_to_dependents = collections.defaultdict(list)
2366 for b in branches.splitlines():
2367 tokens = b.split()
2368 if len(tokens) == 2:
2369 branch_name, tracked = tokens
2370 tracked_to_dependents[tracked].append(branch_name)
2371
2372 print
2373 print 'The dependent local branches of %s are:' % root_branch
2374 dependents = []
2375 def traverse_dependents_preorder(branch, padding=''):
2376 dependents_to_process = tracked_to_dependents.get(branch, [])
2377 padding += ' '
2378 for dependent in dependents_to_process:
2379 print '%s%s' % (padding, dependent)
2380 dependents.append(dependent)
2381 traverse_dependents_preorder(dependent, padding)
2382 traverse_dependents_preorder(root_branch)
2383 print
2384
2385 if not dependents:
2386 print 'There are no dependent local branches for %s' % root_branch
2387 return 0
2388
2389 print ('This command will checkout all dependent branches and run '
2390 '"git cl upload".')
2391 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2392
andybons@chromium.org962f9462016-02-03 20:00:42 +00002393 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002394 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002395 args.extend(['-t', 'Updated patchset dependency'])
2396
rmistry@google.com2dd99862015-06-22 12:22:18 +00002397 # Record all dependents that failed to upload.
2398 failures = {}
2399 # Go through all dependents, checkout the branch and upload.
2400 try:
2401 for dependent_branch in dependents:
2402 print
2403 print '--------------------------------------'
2404 print 'Running "git cl upload" from %s:' % dependent_branch
2405 RunGit(['checkout', '-q', dependent_branch])
2406 print
2407 try:
2408 if CMDupload(OptionParser(), args) != 0:
2409 print 'Upload failed for %s!' % dependent_branch
2410 failures[dependent_branch] = 1
2411 except: # pylint: disable=W0702
2412 failures[dependent_branch] = 1
2413 print
2414 finally:
2415 # Swap back to the original root branch.
2416 RunGit(['checkout', '-q', root_branch])
2417
2418 print
2419 print 'Upload complete for dependent branches!'
2420 for dependent_branch in dependents:
2421 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2422 print ' %s : %s' % (dependent_branch, upload_status)
2423 print
2424
2425 return 0
2426
2427
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002428def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002429 """Show status of changelists.
2430
2431 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002432 - Red not sent for review or broken
2433 - Blue waiting for review
2434 - Yellow waiting for you to reply to review
2435 - Green LGTM'ed
2436 - Magenta in the commit queue
2437 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002438
2439 Also see 'git cl comments'.
2440 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002441 parser.add_option('--field',
2442 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002443 parser.add_option('-f', '--fast', action='store_true',
2444 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002445 parser.add_option(
2446 '-j', '--maxjobs', action='store', type=int,
2447 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002448
2449 auth.add_auth_options(parser)
2450 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002451 if args:
2452 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002453 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002455 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002456 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002457 if options.field.startswith('desc'):
2458 print cl.GetDescription()
2459 elif options.field == 'id':
2460 issueid = cl.GetIssue()
2461 if issueid:
2462 print issueid
2463 elif options.field == 'patch':
2464 patchset = cl.GetPatchset()
2465 if patchset:
2466 print patchset
2467 elif options.field == 'url':
2468 url = cl.GetIssueURL()
2469 if url:
2470 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002471 return 0
2472
2473 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2474 if not branches:
2475 print('No local branch found.')
2476 return 0
2477
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002478 changes = (
2479 Changelist(branchref=b, auth_config=auth_config)
2480 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002481 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002482 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002483 alignment = max(5, max(len(b) for b in branches))
2484 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002485 output = get_cl_statuses(branches,
2486 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002487 max_processes=options.maxjobs,
2488 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002489
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002490 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002491 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002492 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002493 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002494 b, i, status = output.next()
2495 branch_statuses[b] = (i, status)
2496 issue_url, status = branch_statuses.pop(branch)
2497 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002498 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00002499 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00002500 color = ''
2501 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002502 status_str = '(%s)' % status if status else ''
2503 print ' %*s : %s%s %s%s' % (
2504 alignment, ShortBranchName(branch), color, issue_url, status_str,
2505 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002506
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002507 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002508 print
2509 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002510 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002511 if not cl.GetIssue():
2512 print 'No issue assigned.'
2513 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002514 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002515 if not options.fast:
2516 print 'Issue description:'
2517 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002518 return 0
2519
2520
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002521def colorize_CMDstatus_doc():
2522 """To be called once in main() to add colors to git cl status help."""
2523 colors = [i for i in dir(Fore) if i[0].isupper()]
2524
2525 def colorize_line(line):
2526 for color in colors:
2527 if color in line.upper():
2528 # Extract whitespaces first and the leading '-'.
2529 indent = len(line) - len(line.lstrip(' ')) + 1
2530 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2531 return line
2532
2533 lines = CMDstatus.__doc__.splitlines()
2534 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2535
2536
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002537@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002538def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002539 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002540
2541 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002542 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002543 parser.add_option('-r', '--reverse', action='store_true',
2544 help='Lookup the branch(es) for the specified issues. If '
2545 'no issues are specified, all branches with mapped '
2546 'issues will be listed.')
2547 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002548
dnj@chromium.org406c4402015-03-03 17:22:28 +00002549 if options.reverse:
2550 branches = RunGit(['for-each-ref', 'refs/heads',
2551 '--format=%(refname:short)']).splitlines()
2552
2553 # Reverse issue lookup.
2554 issue_branch_map = {}
2555 for branch in branches:
2556 cl = Changelist(branchref=branch)
2557 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2558 if not args:
2559 args = sorted(issue_branch_map.iterkeys())
2560 for issue in args:
2561 if not issue:
2562 continue
2563 print 'Branch for issue number %s: %s' % (
2564 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2565 else:
2566 cl = Changelist()
2567 if len(args) > 0:
2568 try:
2569 issue = int(args[0])
2570 except ValueError:
2571 DieWithError('Pass a number to set the issue or none to list it.\n'
2572 'Maybe you want to run git cl status?')
2573 cl.SetIssue(issue)
2574 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575 return 0
2576
2577
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002578def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002579 """Shows or posts review comments for any changelist."""
2580 parser.add_option('-a', '--add-comment', dest='comment',
2581 help='comment to add to an issue')
2582 parser.add_option('-i', dest='issue',
2583 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002584 parser.add_option('-j', '--json-file',
2585 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002586 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002587 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002588 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002589
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002590 issue = None
2591 if options.issue:
2592 try:
2593 issue = int(options.issue)
2594 except ValueError:
2595 DieWithError('A review issue id is expected to be a number')
2596
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002597 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002598
2599 if options.comment:
2600 cl.AddComment(options.comment)
2601 return 0
2602
2603 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002604 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002605 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002606 summary.append({
2607 'date': message['date'],
2608 'lgtm': False,
2609 'message': message['text'],
2610 'not_lgtm': False,
2611 'sender': message['sender'],
2612 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002613 if message['disapproval']:
2614 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002615 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002616 elif message['approval']:
2617 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002618 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002619 elif message['sender'] == data['owner_email']:
2620 color = Fore.MAGENTA
2621 else:
2622 color = Fore.BLUE
2623 print '\n%s%s %s%s' % (
2624 color, message['date'].split('.', 1)[0], message['sender'],
2625 Fore.RESET)
2626 if message['text'].strip():
2627 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002628 if options.json_file:
2629 with open(options.json_file, 'wb') as f:
2630 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002631 return 0
2632
2633
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002634def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002635 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002636 parser.add_option('-d', '--display', action='store_true',
2637 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002638 auth.add_auth_options(parser)
2639 options, _ = parser.parse_args(args)
2640 auth_config = auth.extract_auth_config_from_options(options)
2641 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002642 if not cl.GetIssue():
2643 DieWithError('This branch has no associated changelist.')
2644 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002645 if options.display:
2646 print description.description
2647 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002648 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002649 if cl.GetDescription() != description.description:
2650 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002651 return 0
2652
2653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002654def CreateDescriptionFromLog(args):
2655 """Pulls out the commit log to use as a base for the CL description."""
2656 log_args = []
2657 if len(args) == 1 and not args[0].endswith('.'):
2658 log_args = [args[0] + '..']
2659 elif len(args) == 1 and args[0].endswith('...'):
2660 log_args = [args[0][:-1]]
2661 elif len(args) == 2:
2662 log_args = [args[0] + '..' + args[1]]
2663 else:
2664 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002665 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002666
2667
thestig@chromium.org44202a22014-03-11 19:22:18 +00002668def CMDlint(parser, args):
2669 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002670 parser.add_option('--filter', action='append', metavar='-x,+y',
2671 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002672 auth.add_auth_options(parser)
2673 options, args = parser.parse_args(args)
2674 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002675
2676 # Access to a protected member _XX of a client class
2677 # pylint: disable=W0212
2678 try:
2679 import cpplint
2680 import cpplint_chromium
2681 except ImportError:
2682 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2683 return 1
2684
2685 # Change the current working directory before calling lint so that it
2686 # shows the correct base.
2687 previous_cwd = os.getcwd()
2688 os.chdir(settings.GetRoot())
2689 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002690 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002691 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2692 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002693 if not files:
2694 print "Cannot lint an empty CL"
2695 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002696
2697 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002698 command = args + files
2699 if options.filter:
2700 command = ['--filter=' + ','.join(options.filter)] + command
2701 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002702
2703 white_regex = re.compile(settings.GetLintRegex())
2704 black_regex = re.compile(settings.GetLintIgnoreRegex())
2705 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2706 for filename in filenames:
2707 if white_regex.match(filename):
2708 if black_regex.match(filename):
2709 print "Ignoring file %s" % filename
2710 else:
2711 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2712 extra_check_functions)
2713 else:
2714 print "Skipping file %s" % filename
2715 finally:
2716 os.chdir(previous_cwd)
2717 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2718 if cpplint._cpplint_state.error_count != 0:
2719 return 1
2720 return 0
2721
2722
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002723def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002724 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002725 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002726 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002727 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002728 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002729 auth.add_auth_options(parser)
2730 options, args = parser.parse_args(args)
2731 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002732
sbc@chromium.org71437c02015-04-09 19:29:40 +00002733 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002734 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002735 return 1
2736
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002737 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738 if args:
2739 base_branch = args[0]
2740 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002741 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002742 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002743
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002744 cl.RunHook(
2745 committing=not options.upload,
2746 may_prompt=False,
2747 verbose=options.verbose,
2748 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002749 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002750
2751
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002752def AddChangeIdToCommitMessage(options, args):
2753 """Re-commits using the current message, assumes the commit hook is in
2754 place.
2755 """
2756 log_desc = options.message or CreateDescriptionFromLog(args)
2757 git_command = ['commit', '--amend', '-m', log_desc]
2758 RunGit(git_command)
2759 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002760 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002761 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002762 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002763 else:
2764 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2765
2766
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002767def GenerateGerritChangeId(message):
2768 """Returns Ixxxxxx...xxx change id.
2769
2770 Works the same way as
2771 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2772 but can be called on demand on all platforms.
2773
2774 The basic idea is to generate git hash of a state of the tree, original commit
2775 message, author/committer info and timestamps.
2776 """
2777 lines = []
2778 tree_hash = RunGitSilent(['write-tree'])
2779 lines.append('tree %s' % tree_hash.strip())
2780 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2781 if code == 0:
2782 lines.append('parent %s' % parent.strip())
2783 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2784 lines.append('author %s' % author.strip())
2785 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2786 lines.append('committer %s' % committer.strip())
2787 lines.append('')
2788 # Note: Gerrit's commit-hook actually cleans message of some lines and
2789 # whitespace. This code is not doing this, but it clearly won't decrease
2790 # entropy.
2791 lines.append(message)
2792 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2793 stdin='\n'.join(lines))
2794 return 'I%s' % change_hash.strip()
2795
2796
piman@chromium.org336f9122014-09-04 02:16:55 +00002797def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002798 """upload the current branch to gerrit."""
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002799 # TODO(tandrii): refactor this to be a method of _GerritChangelistImpl,
2800 # to avoid private members accessors below.
2801
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002802 # We assume the remote called "origin" is the one we want.
2803 # It is probably not worthwhile to support different workflows.
2804 gerrit_remote = 'origin'
2805
luqui@chromium.org609f3952015-05-04 22:47:04 +00002806 remote, remote_branch = cl.GetRemoteBranch()
2807 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2808 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809
andybons@chromium.org962f9462016-02-03 20:00:42 +00002810 if options.title:
2811 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002812 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002813
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002814 if options.squash:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002815 if not cl.GetIssue():
2816 # TODO(tandrii): deperecate this after 2016Q2.
2817 # Backwards compatibility with shadow branch, which used to contain
2818 # change-id for a given branch, using which we can fetch actual issue
2819 # number and set it as the property of the branch, which is the new way.
2820 message = RunGitSilent(['show', '--format=%B', '-s',
2821 'refs/heads/git_cl_uploads/%s' % cl.GetBranch()])
2822 if message:
2823 change_ids = git_footers.get_footer_change_id(message.strip())
2824 if change_ids and len(change_ids) == 1:
2825 details = gerrit_util.GetChangeDetail(
2826 cl._codereview_impl._GetGerritHost(), change_ids[0])
2827 if details:
2828 print('WARNING: found old upload in branch git_cl_uploads/%s '
2829 'corresponding to issue %s' %
2830 (cl.GetBranch(), details['_number']))
2831 cl.SetIssue(details['_number'])
2832 if not cl.GetIssue():
2833 DieWithError(
2834 '\n' # For readability of the blob below.
2835 'Found old upload in branch git_cl_uploads/%s, '
2836 'but failed to find corresponding Gerrit issue.\n'
2837 'If you know the issue number, set it manually first:\n'
2838 ' git cl issue 123456\n'
2839 'If you intended to upload this CL as new issue, '
2840 'just delete or rename the old upload branch:\n'
2841 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2842 'After that, please run git cl upload again.' %
2843 tuple([cl.GetBranch()] * 3))
2844 # End of backwards compatability.
2845
2846 if cl.GetIssue():
2847 # Try to get the message from a previous upload.
2848 message = cl.GetDescription()
2849 if not message:
2850 DieWithError(
2851 'failed to fetch description from current Gerrit issue %d\n'
2852 '%s' % (cl.GetIssue(), cl.GetIssueURL()))
2853 change_id = cl._codereview_impl._GetChangeDetail([])['change_id']
2854 while True:
2855 footer_change_ids = git_footers.get_footer_change_id(message)
2856 if footer_change_ids == [change_id]:
2857 break
2858 if not footer_change_ids:
2859 message = git_footers.add_footer_change_id(message, change_id)
2860 print('WARNING: appended missing Change-Id to issue description')
2861 continue
2862 # There is already a valid footer but with different or several ids.
2863 # Doing this automatically is non-trivial as we don't want to lose
2864 # existing other footers, yet we want to append just 1 desired
2865 # Change-Id. Thus, just create a new footer, but let user verify the new
2866 # description.
2867 message = '%s\n\nChange-Id: %s' % (message, change_id)
2868 print(
2869 'WARNING: issue %s has Change-Id footer(s):\n'
2870 ' %s\n'
2871 'but issue has Change-Id %s, according to Gerrit.\n'
2872 'Please, check the proposed correction to the description, '
2873 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2874 % (cl.GetIssue(), '\n '.join(footer_change_ids), change_id,
2875 change_id))
2876 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2877 if not options.force:
2878 change_desc = ChangeDescription(message)
2879 change_desc.prompt()
2880 message = change_desc.description
2881 if not message:
2882 DieWithError("Description is empty. Aborting...")
2883 # Continue the while loop.
2884 # Sanity check of this code - we should end up with proper message footer.
2885 assert [change_id] == git_footers.get_footer_change_id(message)
2886 change_desc = ChangeDescription(message)
2887 else:
2888 change_desc = ChangeDescription(
2889 options.message or CreateDescriptionFromLog(args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002890 if not options.force:
2891 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002892 if not change_desc.description:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002893 DieWithError("Description is empty. Aborting...")
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002894 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002895 change_ids = git_footers.get_footer_change_id(message)
2896 if len(change_ids) > 1:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002897 DieWithError('too many Change-Id footers, at most 1 allowed.')
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002898 if not change_ids:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002899 # Generate the Change-Id automatically.
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002900 message = git_footers.add_footer_change_id(
2901 message, GenerateGerritChangeId(message))
2902 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002903 change_ids = git_footers.get_footer_change_id(message)
2904 assert len(change_ids) == 1
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002905 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002906
2907 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2908 if remote is '.':
2909 # If our upstream branch is local, we base our squashed commit on its
2910 # squashed version.
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002911 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2912 # Check the squashed hash of the parent.
2913 parent = RunGit(['config',
2914 'branch.%s.gerritsquashhash' % upstream_branch_name],
2915 error_ok=True).strip()
2916 # Verify that the upstream branch has been uploaded too, otherwise
2917 # Gerrit will create additional CLs when uploading.
2918 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2919 RunGitSilent(['rev-parse', parent + ':'])):
2920 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2921 DieWithError(
2922 'Upload upstream branch %s first.\n'
2923 'Note: maybe you\'ve uploaded it with --no-squash or with an old\n'
2924 ' version of depot_tools. If so, then re-upload it with:\n'
2925 ' git cl upload --squash\n' % upstream_branch_name)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002926 else:
2927 parent = cl.GetCommonAncestorWithUpstream()
2928
2929 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2930 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2931 '-m', message]).strip()
2932 else:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002933 change_desc = ChangeDescription(
2934 options.message or CreateDescriptionFromLog(args))
2935 if not change_desc.description:
2936 DieWithError("Description is empty. Aborting...")
2937
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002938 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002939 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002940 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002941 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002942 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002943 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002944
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002945 assert change_desc
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002946 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2947 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002948 if len(commits) > 1:
2949 print('WARNING: This will upload %d commits. Run the following command '
2950 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002951 print('git log %s..%s' % (parent, ref_to_push))
2952 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002953 'commit.')
2954 ask_for_data('About to upload; enter to confirm.')
2955
piman@chromium.org336f9122014-09-04 02:16:55 +00002956 if options.reviewers or options.tbr_owners:
2957 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002958
ukai@chromium.orge8077812012-02-03 03:41:46 +00002959 receive_options = []
2960 cc = cl.GetCCList().split(',')
2961 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002962 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002963 cc = filter(None, cc)
2964 if cc:
2965 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002966 if change_desc.get_reviewers():
2967 receive_options.extend(
2968 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969
ukai@chromium.orge8077812012-02-03 03:41:46 +00002970 git_command = ['push']
2971 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002972 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002973 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002974 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002975 push_stdout = gclient_utils.CheckCallAndFilter(
2976 ['git'] + git_command,
2977 print_stdout=True,
2978 # Flush after every line: useful for seeing progress when running as
2979 # recipe.
2980 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002981
2982 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002983 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2984 change_numbers = [m.group(1)
2985 for m in map(regex.match, push_stdout.splitlines())
2986 if m]
2987 if len(change_numbers) != 1:
2988 DieWithError(
2989 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2990 'Change-Id: %s') % (len(change_numbers), change_id))
2991 cl.SetIssue(change_numbers[0])
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002992 RunGit(['config', 'branch.%s.gerritsquashhash' % cl.GetBranch(),
2993 ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002994 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002996
wittman@chromium.org455dc922015-01-26 20:15:50 +00002997def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2998 """Computes the remote branch ref to use for the CL.
2999
3000 Args:
3001 remote (str): The git remote for the CL.
3002 remote_branch (str): The git remote branch for the CL.
3003 target_branch (str): The target branch specified by the user.
3004 pending_prefix (str): The pending prefix from the settings.
3005 """
3006 if not (remote and remote_branch):
3007 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003008
wittman@chromium.org455dc922015-01-26 20:15:50 +00003009 if target_branch:
3010 # Cannonicalize branch references to the equivalent local full symbolic
3011 # refs, which are then translated into the remote full symbolic refs
3012 # below.
3013 if '/' not in target_branch:
3014 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3015 else:
3016 prefix_replacements = (
3017 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3018 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3019 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3020 )
3021 match = None
3022 for regex, replacement in prefix_replacements:
3023 match = re.search(regex, target_branch)
3024 if match:
3025 remote_branch = target_branch.replace(match.group(0), replacement)
3026 break
3027 if not match:
3028 # This is a branch path but not one we recognize; use as-is.
3029 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003030 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3031 # Handle the refs that need to land in different refs.
3032 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003033
wittman@chromium.org455dc922015-01-26 20:15:50 +00003034 # Create the true path to the remote branch.
3035 # Does the following translation:
3036 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3037 # * refs/remotes/origin/master -> refs/heads/master
3038 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3039 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3040 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3041 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3042 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3043 'refs/heads/')
3044 elif remote_branch.startswith('refs/remotes/branch-heads'):
3045 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3046 # If a pending prefix exists then replace refs/ with it.
3047 if pending_prefix:
3048 remote_branch = remote_branch.replace('refs/', pending_prefix)
3049 return remote_branch
3050
3051
piman@chromium.org336f9122014-09-04 02:16:55 +00003052def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003053 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003054 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003055 upload_args.extend(['--server', cl.GetCodereviewServer()])
3056 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
3057 upload_args.extend(auth.auth_config_to_command_options(
3058 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059 if options.emulate_svn_auto_props:
3060 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003061
3062 change_desc = None
3063
pgervais@chromium.org91141372014-01-09 23:27:20 +00003064 if options.email is not None:
3065 upload_args.extend(['--email', options.email])
3066
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003067 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003068 if options.title:
3069 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00003070 if options.message:
3071 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00003072 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003073 print ("This branch is associated with issue %s. "
3074 "Adding patch to that issue." % cl.GetIssue())
3075 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003076 if options.title:
3077 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00003078 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00003080 if options.reviewers or options.tbr_owners:
3081 change_desc.update_reviewers(options.reviewers,
3082 options.tbr_owners,
3083 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003084 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003085 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003086
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003087 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003088 print "Description is empty; aborting."
3089 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003090
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003091 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003092 if change_desc.get_reviewers():
3093 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00003094 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003095 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00003096 DieWithError("Must specify reviewers to send email.")
3097 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00003098
3099 # We check this before applying rietveld.private assuming that in
3100 # rietveld.cc only addresses which we can send private CLs to are listed
3101 # if rietveld.private is set, and so we should ignore rietveld.cc only when
3102 # --private is specified explicitly on the command line.
3103 if options.private:
3104 logging.warn('rietveld.cc is ignored since private flag is specified. '
3105 'You need to review and add them manually if necessary.')
3106 cc = cl.GetCCListWithoutDefault()
3107 else:
3108 cc = cl.GetCCList()
3109 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00003110 if cc:
3111 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003112
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003113 if options.private or settings.GetDefaultPrivateFlag() == "True":
3114 upload_args.append('--private')
3115
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003116 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00003117 if not options.find_copies:
3118 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003120 # Include the upstream repo's URL in the change -- this is useful for
3121 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003122 remote_url = cl.GetGitBaseUrlFromConfig()
3123 if not remote_url:
3124 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003125 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003126 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00003127 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
3128 remote_url = (cl.GetRemoteUrl() + '@'
3129 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003130 if remote_url:
3131 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00003132 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00003133 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
3134 settings.GetPendingRefPrefix())
3135 if target_ref:
3136 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003137
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003138 # Look for dependent patchsets. See crbug.com/480453 for more details.
3139 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3140 upstream_branch = ShortBranchName(upstream_branch)
3141 if remote is '.':
3142 # A local branch is being tracked.
3143 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00003144 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003145 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003146 print ('Skipping dependency patchset upload because git config '
3147 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003148 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003149 else:
3150 auth_config = auth.extract_auth_config_from_options(options)
3151 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
3152 branch_cl_issue_url = branch_cl.GetIssueURL()
3153 branch_cl_issue = branch_cl.GetIssue()
3154 branch_cl_patchset = branch_cl.GetPatchset()
3155 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
3156 upload_args.extend(
3157 ['--depends_on_patchset', '%s:%s' % (
3158 branch_cl_issue, branch_cl_patchset)])
3159 print
3160 print ('The current branch (%s) is tracking a local branch (%s) with '
3161 'an associated CL.') % (cl.GetBranch(), local_branch)
3162 print 'Adding %s/#ps%s as a dependency patchset.' % (
3163 branch_cl_issue_url, branch_cl_patchset)
3164 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003165
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003166 project = settings.GetProject()
3167 if project:
3168 upload_args.extend(['--project', project])
3169
rmistry@google.comef966222015-04-07 11:15:01 +00003170 if options.cq_dry_run:
3171 upload_args.extend(['--cq_dry_run'])
3172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003173 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00003174 upload_args = ['upload'] + upload_args + args
3175 logging.info('upload.RealMain(%s)', upload_args)
3176 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00003177 issue = int(issue)
3178 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00003179 except KeyboardInterrupt:
3180 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 except:
3182 # If we got an exception after the user typed a description for their
3183 # change, back up the description before re-raising.
3184 if change_desc:
3185 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
3186 print '\nGot exception while uploading -- saving description to %s\n' \
3187 % backup_path
3188 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003189 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003190 backup_file.close()
3191 raise
3192
3193 if not cl.GetIssue():
3194 cl.SetIssue(issue)
3195 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003196
3197 if options.use_commit_queue:
3198 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003199 return 0
3200
3201
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003202def cleanup_list(l):
3203 """Fixes a list so that comma separated items are put as individual items.
3204
3205 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3206 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3207 """
3208 items = sum((i.split(',') for i in l), [])
3209 stripped_items = (i.strip() for i in items)
3210 return sorted(filter(None, stripped_items))
3211
3212
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003213@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003214def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003215 """Uploads the current changelist to codereview.
3216
3217 Can skip dependency patchset uploads for a branch by running:
3218 git config branch.branch_name.skip-deps-uploads True
3219 To unset run:
3220 git config --unset branch.branch_name.skip-deps-uploads
3221 Can also set the above globally by using the --global flag.
3222 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003223 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3224 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003225 parser.add_option('--bypass-watchlists', action='store_true',
3226 dest='bypass_watchlists',
3227 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003228 parser.add_option('-f', action='store_true', dest='force',
3229 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003230 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003231 parser.add_option('-t', dest='title',
3232 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003233 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003234 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003235 help='reviewer email addresses')
3236 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003237 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003238 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003239 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003240 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003241 parser.add_option('--emulate_svn_auto_props',
3242 '--emulate-svn-auto-props',
3243 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003244 dest="emulate_svn_auto_props",
3245 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003246 parser.add_option('-c', '--use-commit-queue', action='store_true',
3247 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003248 parser.add_option('--private', action='store_true',
3249 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003250 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003251 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003252 metavar='TARGET',
3253 help='Apply CL to remote ref TARGET. ' +
3254 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003255 parser.add_option('--squash', action='store_true',
3256 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003257 parser.add_option('--no-squash', action='store_true',
3258 help='Don\'t squash multiple commits into one ' +
3259 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003260 parser.add_option('--email', default=None,
3261 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003262 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3263 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003264 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3265 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003266 help='Send the patchset to do a CQ dry run right after '
3267 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003268 parser.add_option('--dependencies', action='store_true',
3269 help='Uploads CLs of all the local branches that depend on '
3270 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003271
rmistry@google.com2dd99862015-06-22 12:22:18 +00003272 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003273 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003274 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003275 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003276 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003277
sbc@chromium.org71437c02015-04-09 19:29:40 +00003278 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003279 return 1
3280
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003281 options.reviewers = cleanup_list(options.reviewers)
3282 options.cc = cleanup_list(options.cc)
3283
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003284 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3285 settings.GetIsGerrit()
3286
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003287 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003288 if args:
3289 # TODO(ukai): is it ok for gerrit case?
3290 base_branch = args[0]
3291 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003292 if cl.GetBranch() is None:
3293 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3294
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003295 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003296 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003297 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003298
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003299 # Make sure authenticated to Rietveld before running expensive hooks. It is
3300 # a fast, best efforts check. Rietveld still can reject the authentication
3301 # during the actual upload.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003302 if not cl.IsGerrit() and auth_config.use_oauth2:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003303 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003304 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003305 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003306 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003307
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003308 # Apply watchlists on upload.
3309 change = cl.GetChange(base_branch, None)
3310 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3311 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003312 if not options.bypass_watchlists:
3313 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003314
ukai@chromium.orge8077812012-02-03 03:41:46 +00003315 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003316 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003317 # Set the reviewer list now so that presubmit checks can access it.
3318 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003319 change_description.update_reviewers(options.reviewers,
3320 options.tbr_owners,
3321 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003322 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003323 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003324 may_prompt=not options.force,
3325 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003326 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003327 if not hook_results.should_continue():
3328 return 1
3329 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003330 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003331
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003332 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003333 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003334 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003335 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003336 print ('The last upload made from this repository was patchset #%d but '
3337 'the most recent patchset on the server is #%d.'
3338 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003339 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3340 'from another machine or branch the patch you\'re uploading now '
3341 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003342 ask_for_data('About to upload; enter to confirm.')
3343
iannucci@chromium.org79540052012-10-19 23:15:26 +00003344 print_stats(options.similarity, options.find_copies, args)
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003345 if cl.IsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003346 if options.squash and options.no_squash:
3347 DieWithError('Can only use one of --squash or --no-squash')
3348
3349 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3350 not options.no_squash)
3351
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00003352 ret = GerritUpload(options, args, cl, change)
3353 else:
3354 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003355 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003356 git_set_branch_value('last-upload-hash',
3357 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003358 # Run post upload hooks, if specified.
3359 if settings.GetRunPostUploadHook():
3360 presubmit_support.DoPostUploadExecuter(
3361 change,
3362 cl,
3363 settings.GetRoot(),
3364 options.verbose,
3365 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003366
rmistry@google.com2dd99862015-06-22 12:22:18 +00003367 # Upload all dependencies if specified.
3368 if options.dependencies:
3369 print
3370 print '--dependencies has been specified.'
3371 print 'All dependent local branches will be re-uploaded.'
3372 print
3373 # Remove the dependencies flag from args so that we do not end up in a
3374 # loop.
3375 orig_args.remove('--dependencies')
3376 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003377 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003378
3379
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003380def IsSubmoduleMergeCommit(ref):
3381 # When submodules are added to the repo, we expect there to be a single
3382 # non-git-svn merge commit at remote HEAD with a signature comment.
3383 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003384 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003385 return RunGit(cmd) != ''
3386
3387
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003388def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003389 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003390
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003391 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3392 upstream and closes the issue automatically and atomically.
3393
3394 Otherwise (in case of Rietveld):
3395 Squashes branch into a single commit.
3396 Updates changelog with metadata (e.g. pointer to review).
3397 Pushes/dcommits the code upstream.
3398 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003399 """
3400 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3401 help='bypass upload presubmit hook')
3402 parser.add_option('-m', dest='message',
3403 help="override review description")
3404 parser.add_option('-f', action='store_true', dest='force',
3405 help="force yes to questions (don't prompt)")
3406 parser.add_option('-c', dest='contributor',
3407 help="external contributor for patch (appended to " +
3408 "description and used as author for git). Should be " +
3409 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003410 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003411 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003412 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003413 auth_config = auth.extract_auth_config_from_options(options)
3414
3415 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003416
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003417 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3418 if cl.IsGerrit():
3419 if options.message:
3420 # This could be implemented, but it requires sending a new patch to
3421 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3422 # Besides, Gerrit has the ability to change the commit message on submit
3423 # automatically, thus there is no need to support this option (so far?).
3424 parser.error('-m MESSAGE option is not supported for Gerrit.')
3425 if options.contributor:
3426 parser.error(
3427 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3428 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3429 'the contributor\'s "name <email>". If you can\'t upload such a '
3430 'commit for review, contact your repository admin and request'
3431 '"Forge-Author" permission.')
3432 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3433 options.verbose)
3434
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003435 current = cl.GetBranch()
3436 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3437 if not settings.GetIsGitSvn() and remote == '.':
3438 print
3439 print 'Attempting to push branch %r into another local branch!' % current
3440 print
3441 print 'Either reparent this branch on top of origin/master:'
3442 print ' git reparent-branch --root'
3443 print
3444 print 'OR run `git rebase-update` if you think the parent branch is already'
3445 print 'committed.'
3446 print
3447 print ' Current parent: %r' % upstream_branch
3448 return 1
3449
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003450 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003451 # Default to merging against our best guess of the upstream branch.
3452 args = [cl.GetUpstreamBranch()]
3453
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003454 if options.contributor:
3455 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3456 print "Please provide contibutor as 'First Last <email@example.com>'"
3457 return 1
3458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003459 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003460 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003461
sbc@chromium.org71437c02015-04-09 19:29:40 +00003462 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003463 return 1
3464
3465 # This rev-list syntax means "show all commits not in my branch that
3466 # are in base_branch".
3467 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3468 base_branch]).splitlines()
3469 if upstream_commits:
3470 print ('Base branch "%s" has %d commits '
3471 'not in this branch.' % (base_branch, len(upstream_commits)))
3472 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3473 return 1
3474
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003475 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003476 svn_head = None
3477 if cmd == 'dcommit' or base_has_submodules:
3478 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3479 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003480
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003481 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003482 # If the base_head is a submodule merge commit, the first parent of the
3483 # base_head should be a git-svn commit, which is what we're interested in.
3484 base_svn_head = base_branch
3485 if base_has_submodules:
3486 base_svn_head += '^1'
3487
3488 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003489 if extra_commits:
3490 print ('This branch has %d additional commits not upstreamed yet.'
3491 % len(extra_commits.splitlines()))
3492 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3493 'before attempting to %s.' % (base_branch, cmd))
3494 return 1
3495
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003496 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003497 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003498 author = None
3499 if options.contributor:
3500 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003501 hook_results = cl.RunHook(
3502 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003503 may_prompt=not options.force,
3504 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003505 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003506 if not hook_results.should_continue():
3507 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003508
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003509 # Check the tree status if the tree status URL is set.
3510 status = GetTreeStatus()
3511 if 'closed' == status:
3512 print('The tree is closed. Please wait for it to reopen. Use '
3513 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3514 return 1
3515 elif 'unknown' == status:
3516 print('Unable to determine tree status. Please verify manually and '
3517 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3518 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003519
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003520 change_desc = ChangeDescription(options.message)
3521 if not change_desc.description and cl.GetIssue():
3522 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003524 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003525 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003526 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003527 else:
3528 print 'No description set.'
3529 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3530 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003531
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003532 # Keep a separate copy for the commit message, because the commit message
3533 # contains the link to the Rietveld issue, while the Rietveld message contains
3534 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003535 # Keep a separate copy for the commit message.
3536 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003537 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003538
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003539 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003540 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003541 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003542 # after it. Add a period on a new line to circumvent this. Also add a space
3543 # before the period to make sure that Gitiles continues to correctly resolve
3544 # the URL.
3545 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003547 commit_desc.append_footer('Patch from %s.' % options.contributor)
3548
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003549 print('Description:')
3550 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003551
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003552 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003553 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003554 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003555
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003556 # We want to squash all this branch's commits into one commit with the proper
3557 # description. We do this by doing a "reset --soft" to the base branch (which
3558 # keeps the working copy the same), then dcommitting that. If origin/master
3559 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3560 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003562 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3563 # Delete the branches if they exist.
3564 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3565 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3566 result = RunGitWithCode(showref_cmd)
3567 if result[0] == 0:
3568 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003569
3570 # We might be in a directory that's present in this branch but not in the
3571 # trunk. Move up to the top of the tree so that git commands that expect a
3572 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003573 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003574 if rel_base_path:
3575 os.chdir(rel_base_path)
3576
3577 # Stuff our change into the merge branch.
3578 # We wrap in a try...finally block so if anything goes wrong,
3579 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003580 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003581 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003582 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003583 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003584 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003585 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003586 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003588 RunGit(
3589 [
3590 'commit', '--author', options.contributor,
3591 '-m', commit_desc.description,
3592 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003593 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003594 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003595 if base_has_submodules:
3596 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3597 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3598 RunGit(['checkout', CHERRY_PICK_BRANCH])
3599 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003600 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003601 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003602 mirror = settings.GetGitMirror(remote)
3603 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003604 pending_prefix = settings.GetPendingRefPrefix()
3605 if not pending_prefix or branch.startswith(pending_prefix):
3606 # If not using refs/pending/heads/* at all, or target ref is already set
3607 # to pending, then push to the target ref directly.
3608 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003609 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003610 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003611 else:
3612 # Cherry-pick the change on top of pending ref and then push it.
3613 assert branch.startswith('refs/'), branch
3614 assert pending_prefix[-1] == '/', pending_prefix
3615 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003616 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003617 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003618 if retcode == 0:
3619 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003620 else:
3621 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003622 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003623 'svn', 'dcommit',
3624 '-C%s' % options.similarity,
3625 '--no-rebase', '--rmdir',
3626 ]
3627 if settings.GetForceHttpsCommitUrl():
3628 # Allow forcing https commit URLs for some projects that don't allow
3629 # committing to http URLs (like Google Code).
3630 remote_url = cl.GetGitSvnRemoteUrl()
3631 if urlparse.urlparse(remote_url).scheme == 'http':
3632 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003633 cmd_args.append('--commit-url=%s' % remote_url)
3634 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003635 if 'Committed r' in output:
3636 revision = re.match(
3637 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3638 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003639 finally:
3640 # And then swap back to the original branch and clean up.
3641 RunGit(['checkout', '-q', cl.GetBranch()])
3642 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003643 if base_has_submodules:
3644 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003646 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003647 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003648 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003649
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003650 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003651 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003652 try:
3653 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3654 # We set pushed_to_pending to False, since it made it all the way to the
3655 # real ref.
3656 pushed_to_pending = False
3657 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003658 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003659
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003660 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003661 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003663 if not to_pending:
3664 if viewvc_url and revision:
3665 change_desc.append_footer(
3666 'Committed: %s%s' % (viewvc_url, revision))
3667 elif revision:
3668 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669 print ('Closing issue '
3670 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003671 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003672 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003673 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003674 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003675 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003676 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003677 if options.bypass_hooks:
3678 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3679 else:
3680 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003681 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003682 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003683
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003684 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003685 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3686 print 'The commit is in the pending queue (%s).' % pending_ref
3687 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003688 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003689 'footer.' % branch)
3690
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003691 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3692 if os.path.isfile(hook):
3693 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003694
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003695 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003696
3697
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003698def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3699 print
3700 print 'Waiting for commit to be landed on %s...' % real_ref
3701 print '(If you are impatient, you may Ctrl-C once without harm)'
3702 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3703 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003704 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003705
3706 loop = 0
3707 while True:
3708 sys.stdout.write('fetching (%d)... \r' % loop)
3709 sys.stdout.flush()
3710 loop += 1
3711
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003712 if mirror:
3713 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003714 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3715 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3716 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3717 for commit in commits.splitlines():
3718 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3719 print 'Found commit on %s' % real_ref
3720 return commit
3721
3722 current_rev = to_rev
3723
3724
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003725def PushToGitPending(remote, pending_ref, upstream_ref):
3726 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3727
3728 Returns:
3729 (retcode of last operation, output log of last operation).
3730 """
3731 assert pending_ref.startswith('refs/'), pending_ref
3732 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3733 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3734 code = 0
3735 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003736 max_attempts = 3
3737 attempts_left = max_attempts
3738 while attempts_left:
3739 if attempts_left != max_attempts:
3740 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3741 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003742
3743 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003744 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003745 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003746 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003747 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003748 print 'Fetch failed with exit code %d.' % code
3749 if out.strip():
3750 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003751 continue
3752
3753 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003754 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003755 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003756 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003757 if code:
3758 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003759 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3760 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003761 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3762 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003763 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003764 return code, out
3765
3766 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003767 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003768 code, out = RunGitWithCode(
3769 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3770 if code == 0:
3771 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003772 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003773 return code, out
3774
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003775 print 'Push failed with exit code %d.' % code
3776 if out.strip():
3777 print out.strip()
3778 if IsFatalPushFailure(out):
3779 print (
3780 'Fatal push error. Make sure your .netrc credentials and git '
3781 'user.email are correct and you have push access to the repo.')
3782 return code, out
3783
3784 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003785 return code, out
3786
3787
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003788def IsFatalPushFailure(push_stdout):
3789 """True if retrying push won't help."""
3790 return '(prohibited by Gerrit)' in push_stdout
3791
3792
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003793@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003794def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003795 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003796 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003797 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003798 # If it looks like previous commits were mirrored with git-svn.
3799 message = """This repository appears to be a git-svn mirror, but no
3800upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3801 else:
3802 message = """This doesn't appear to be an SVN repository.
3803If your project has a true, writeable git repository, you probably want to run
3804'git cl land' instead.
3805If your project has a git mirror of an upstream SVN master, you probably need
3806to run 'git svn init'.
3807
3808Using the wrong command might cause your commit to appear to succeed, and the
3809review to be closed, without actually landing upstream. If you choose to
3810proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003811 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003812 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813 return SendUpstream(parser, args, 'dcommit')
3814
3815
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003816@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003817def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003818 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003819 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003820 print('This appears to be an SVN repository.')
3821 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003822 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003823 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003824 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003825
3826
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003827@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003828def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003829 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003830 parser.add_option('-b', dest='newbranch',
3831 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003832 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003834 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3835 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003836 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003837 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003838 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003839 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003840 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003841 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003842
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003843
3844 group = optparse.OptionGroup(
3845 parser,
3846 'Options for continuing work on the current issue uploaded from a '
3847 'different clone (e.g. different machine). Must be used independently '
3848 'from the other options. No issue number should be specified, and the '
3849 'branch must have an issue number associated with it')
3850 group.add_option('--reapply', action='store_true', dest='reapply',
3851 help='Reset the branch and reapply the issue.\n'
3852 'CAUTION: This will undo any local changes in this '
3853 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003854
3855 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003856 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003857 parser.add_option_group(group)
3858
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003859 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003861 auth_config = auth.extract_auth_config_from_options(options)
3862
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003863 cl = Changelist(auth_config=auth_config)
3864
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003865 issue_arg = None
3866 if options.reapply :
3867 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003868 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003869
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003870 issue_arg = cl.GetIssue()
3871 upstream = cl.GetUpstreamBranch()
3872 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003873 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003874
3875 RunGit(['reset', '--hard', upstream])
3876 if options.pull:
3877 RunGit(['pull'])
3878 else:
3879 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003880 parser.error('Must specify issue number or url')
3881 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003882
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003883 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003884 parser.print_help()
3885 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003886
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003887 if cl.IsGerrit():
3888 if options.reject:
3889 parser.error('--reject is not supported with Gerrit codereview.')
3890 if options.nocommit:
3891 parser.error('--nocommit is not supported with Gerrit codereview.')
3892 if options.directory:
3893 parser.error('--directory is not supported with Gerrit codereview.')
3894
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003895 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003896 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003897 return 1
3898
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003899 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003900 if options.reapply:
3901 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003902 if options.force:
3903 RunGit(['branch', '-D', options.newbranch],
3904 stderr=subprocess2.PIPE, error_ok=True)
3905 RunGit(['checkout', '-b', options.newbranch,
3906 Changelist().GetUpstreamBranch()])
3907
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003908 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3909 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910
3911
3912def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003913 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003914 # Provide a wrapper for git svn rebase to help avoid accidental
3915 # git svn dcommit.
3916 # It's the only command that doesn't use parser at all since we just defer
3917 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003918
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003919 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003920
3921
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003922def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923 """Fetches the tree status and returns either 'open', 'closed',
3924 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003925 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926 if url:
3927 status = urllib2.urlopen(url).read().lower()
3928 if status.find('closed') != -1 or status == '0':
3929 return 'closed'
3930 elif status.find('open') != -1 or status == '1':
3931 return 'open'
3932 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003933 return 'unset'
3934
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003935
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003936def GetTreeStatusReason():
3937 """Fetches the tree status from a json url and returns the message
3938 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003939 url = settings.GetTreeStatusUrl()
3940 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003941 connection = urllib2.urlopen(json_url)
3942 status = json.loads(connection.read())
3943 connection.close()
3944 return status['message']
3945
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003946
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003947def GetBuilderMaster(bot_list):
3948 """For a given builder, fetch the master from AE if available."""
3949 map_url = 'https://builders-map.appspot.com/'
3950 try:
3951 master_map = json.load(urllib2.urlopen(map_url))
3952 except urllib2.URLError as e:
3953 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3954 (map_url, e))
3955 except ValueError as e:
3956 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3957 if not master_map:
3958 return None, 'Failed to build master map.'
3959
3960 result_master = ''
3961 for bot in bot_list:
3962 builder = bot.split(':', 1)[0]
3963 master_list = master_map.get(builder, [])
3964 if not master_list:
3965 return None, ('No matching master for builder %s.' % builder)
3966 elif len(master_list) > 1:
3967 return None, ('The builder name %s exists in multiple masters %s.' %
3968 (builder, master_list))
3969 else:
3970 cur_master = master_list[0]
3971 if not result_master:
3972 result_master = cur_master
3973 elif result_master != cur_master:
3974 return None, 'The builders do not belong to the same master.'
3975 return result_master, None
3976
3977
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003978def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003979 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003980 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003981 status = GetTreeStatus()
3982 if 'unset' == status:
3983 print 'You must configure your tree status URL by running "git cl config".'
3984 return 2
3985
3986 print "The tree is %s" % status
3987 print
3988 print GetTreeStatusReason()
3989 if status != 'open':
3990 return 1
3991 return 0
3992
3993
maruel@chromium.org15192402012-09-06 12:38:29 +00003994def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003995 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003996 group = optparse.OptionGroup(parser, "Try job options")
3997 group.add_option(
3998 "-b", "--bot", action="append",
3999 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4000 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004001 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004002 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004003 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004004 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004005 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004006 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004007 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004008 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004009 "-r", "--revision",
4010 help="Revision to use for the try job; default: the "
4011 "revision will be determined by the try server; see "
4012 "its waterfall for more info")
4013 group.add_option(
4014 "-c", "--clobber", action="store_true", default=False,
4015 help="Force a clobber before building; e.g. don't do an "
4016 "incremental build")
4017 group.add_option(
4018 "--project",
4019 help="Override which project to use. Projects are defined "
4020 "server-side to define what default bot set to use")
4021 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004022 "-p", "--property", dest="properties", action="append", default=[],
4023 help="Specify generic properties in the form -p key1=value1 -p "
4024 "key2=value2 etc (buildbucket only). The value will be treated as "
4025 "json if decodable, or as string otherwise.")
4026 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004027 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004028 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004029 "--use-rietveld", action="store_true", default=False,
4030 help="Use Rietveld to trigger try jobs.")
4031 group.add_option(
4032 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4033 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004034 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004035 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004036 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004037 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004038
machenbach@chromium.org45453142015-09-15 08:45:22 +00004039 if options.use_rietveld and options.properties:
4040 parser.error('Properties can only be specified with buildbucket')
4041
4042 # Make sure that all properties are prop=value pairs.
4043 bad_params = [x for x in options.properties if '=' not in x]
4044 if bad_params:
4045 parser.error('Got properties with missing "=": %s' % bad_params)
4046
maruel@chromium.org15192402012-09-06 12:38:29 +00004047 if args:
4048 parser.error('Unknown arguments: %s' % args)
4049
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004050 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004051 if not cl.GetIssue():
4052 parser.error('Need to upload first')
4053
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004054 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004055 if props.get('closed'):
4056 parser.error('Cannot send tryjobs for a closed CL')
4057
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004058 if props.get('private'):
4059 parser.error('Cannot use trybots with private issue')
4060
maruel@chromium.org15192402012-09-06 12:38:29 +00004061 if not options.name:
4062 options.name = cl.GetBranch()
4063
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004064 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004065 options.master, err_msg = GetBuilderMaster(options.bot)
4066 if err_msg:
4067 parser.error('Tryserver master cannot be found because: %s\n'
4068 'Please manually specify the tryserver master'
4069 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004070
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004071 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004072 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004073 if not options.bot:
4074 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004075
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004076 # Get try masters from PRESUBMIT.py files.
4077 masters = presubmit_support.DoGetTryMasters(
4078 change,
4079 change.LocalPaths(),
4080 settings.GetRoot(),
4081 None,
4082 None,
4083 options.verbose,
4084 sys.stdout)
4085 if masters:
4086 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004087
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004088 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4089 options.bot = presubmit_support.DoGetTrySlaves(
4090 change,
4091 change.LocalPaths(),
4092 settings.GetRoot(),
4093 None,
4094 None,
4095 options.verbose,
4096 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004097
4098 if not options.bot:
4099 # Get try masters from cq.cfg if any.
4100 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4101 # location.
4102 cq_cfg = os.path.join(change.RepositoryRoot(),
4103 'infra', 'config', 'cq.cfg')
4104 if os.path.exists(cq_cfg):
4105 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004106 cq_masters = commit_queue.get_master_builder_map(
4107 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004108 for master, builders in cq_masters.iteritems():
4109 for builder in builders:
4110 # Skip presubmit builders, because these will fail without LGTM.
4111 if 'presubmit' not in builder.lower():
4112 masters.setdefault(master, {})[builder] = ['defaulttests']
4113 if masters:
4114 return masters
4115
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004116 if not options.bot:
4117 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004118
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004119 builders_and_tests = {}
4120 # TODO(machenbach): The old style command-line options don't support
4121 # multiple try masters yet.
4122 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4123 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4124
4125 for bot in old_style:
4126 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004127 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004128 elif ',' in bot:
4129 parser.error('Specify one bot per --bot flag')
4130 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004131 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004132
4133 for bot, tests in new_style:
4134 builders_and_tests.setdefault(bot, []).extend(tests)
4135
4136 # Return a master map with one master to be backwards compatible. The
4137 # master name defaults to an empty string, which will cause the master
4138 # not to be set on rietveld (deprecated).
4139 return {options.master: builders_and_tests}
4140
4141 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004142
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004143 for builders in masters.itervalues():
4144 if any('triggered' in b for b in builders):
4145 print >> sys.stderr, (
4146 'ERROR You are trying to send a job to a triggered bot. This type of'
4147 ' bot requires an\ninitial job from a parent (usually a builder). '
4148 'Instead send your job to the parent.\n'
4149 'Bot list: %s' % builders)
4150 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004151
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004152 patchset = cl.GetMostRecentPatchset()
4153 if patchset and patchset != cl.GetPatchset():
4154 print(
4155 '\nWARNING Mismatch between local config and server. Did a previous '
4156 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4157 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004158 if options.luci:
4159 trigger_luci_job(cl, masters, options)
4160 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004161 try:
4162 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4163 except BuildbucketResponseException as ex:
4164 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004165 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004166 except Exception as e:
4167 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4168 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4169 e, stacktrace)
4170 return 1
4171 else:
4172 try:
4173 cl.RpcServer().trigger_distributed_try_jobs(
4174 cl.GetIssue(), patchset, options.name, options.clobber,
4175 options.revision, masters)
4176 except urllib2.HTTPError as e:
4177 if e.code == 404:
4178 print('404 from rietveld; '
4179 'did you mean to use "git try" instead of "git cl try"?')
4180 return 1
4181 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004182
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004183 for (master, builders) in sorted(masters.iteritems()):
4184 if master:
4185 print 'Master: %s' % master
4186 length = max(len(builder) for builder in builders)
4187 for builder in sorted(builders):
4188 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004189 return 0
4190
4191
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004192def CMDtry_results(parser, args):
4193 group = optparse.OptionGroup(parser, "Try job results options")
4194 group.add_option(
4195 "-p", "--patchset", type=int, help="patchset number if not current.")
4196 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004197 "--print-master", action='store_true', help="print master name as well.")
4198 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004199 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004200 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004201 group.add_option(
4202 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4203 help="Host of buildbucket. The default host is %default.")
4204 parser.add_option_group(group)
4205 auth.add_auth_options(parser)
4206 options, args = parser.parse_args(args)
4207 if args:
4208 parser.error('Unrecognized args: %s' % ' '.join(args))
4209
4210 auth_config = auth.extract_auth_config_from_options(options)
4211 cl = Changelist(auth_config=auth_config)
4212 if not cl.GetIssue():
4213 parser.error('Need to upload first')
4214
4215 if not options.patchset:
4216 options.patchset = cl.GetMostRecentPatchset()
4217 if options.patchset and options.patchset != cl.GetPatchset():
4218 print(
4219 '\nWARNING Mismatch between local config and server. Did a previous '
4220 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4221 'Continuing using\npatchset %s.\n' % options.patchset)
4222 try:
4223 jobs = fetch_try_jobs(auth_config, cl, options)
4224 except BuildbucketResponseException as ex:
4225 print 'Buildbucket error: %s' % ex
4226 return 1
4227 except Exception as e:
4228 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4229 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4230 e, stacktrace)
4231 return 1
4232 print_tryjobs(options, jobs)
4233 return 0
4234
4235
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004236@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004237def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004238 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004239 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004240 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004241 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004244 if args:
4245 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004246 branch = cl.GetBranch()
4247 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004248 cl = Changelist()
4249 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004250
4251 # Clear configured merge-base, if there is one.
4252 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004253 else:
4254 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004255 return 0
4256
4257
thestig@chromium.org00858c82013-12-02 23:08:03 +00004258def CMDweb(parser, args):
4259 """Opens the current CL in the web browser."""
4260 _, args = parser.parse_args(args)
4261 if args:
4262 parser.error('Unrecognized args: %s' % ' '.join(args))
4263
4264 issue_url = Changelist().GetIssueURL()
4265 if not issue_url:
4266 print >> sys.stderr, 'ERROR No issue to open'
4267 return 1
4268
4269 webbrowser.open(issue_url)
4270 return 0
4271
4272
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004273def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004274 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004275 auth.add_auth_options(parser)
4276 options, args = parser.parse_args(args)
4277 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004278 if args:
4279 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004280 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004281 props = cl.GetIssueProperties()
4282 if props.get('private'):
4283 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004284 cl.SetFlag('commit', '1')
4285 return 0
4286
4287
groby@chromium.org411034a2013-02-26 15:12:01 +00004288def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004289 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004290 auth.add_auth_options(parser)
4291 options, args = parser.parse_args(args)
4292 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004293 if args:
4294 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004295 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004296 # Ensure there actually is an issue to close.
4297 cl.GetDescription()
4298 cl.CloseIssue()
4299 return 0
4300
4301
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004302def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004303 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004304 auth.add_auth_options(parser)
4305 options, args = parser.parse_args(args)
4306 auth_config = auth.extract_auth_config_from_options(options)
4307 if args:
4308 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004309
4310 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004311 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004312 # Staged changes would be committed along with the patch from last
4313 # upload, hence counted toward the "last upload" side in the final
4314 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004315 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004316 return 1
4317
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004318 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004319 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004320 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004321 if not issue:
4322 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004323 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004324 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004325
4326 # Create a new branch based on the merge-base
4327 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004328 # Clear cached branch in cl object, to avoid overwriting original CL branch
4329 # properties.
4330 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004331 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004332 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004333 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004334 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004335 return rtn
4336
wychen@chromium.org06928532015-02-03 02:11:29 +00004337 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004338 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004339 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004340 finally:
4341 RunGit(['checkout', '-q', branch])
4342 RunGit(['branch', '-D', TMP_BRANCH])
4343
4344 return 0
4345
4346
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004347def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004348 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004349 parser.add_option(
4350 '--no-color',
4351 action='store_true',
4352 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004353 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004354 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004355 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004356
4357 author = RunGit(['config', 'user.email']).strip() or None
4358
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004359 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004360
4361 if args:
4362 if len(args) > 1:
4363 parser.error('Unknown args')
4364 base_branch = args[0]
4365 else:
4366 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004367 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004368
4369 change = cl.GetChange(base_branch, None)
4370 return owners_finder.OwnersFinder(
4371 [f.LocalPath() for f in
4372 cl.GetChange(base_branch, None).AffectedFiles()],
4373 change.RepositoryRoot(), author,
4374 fopen=file, os_path=os.path, glob=glob.glob,
4375 disable_color=options.no_color).run()
4376
4377
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004378def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004379 """Generates a diff command."""
4380 # Generate diff for the current branch's changes.
4381 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4382 upstream_commit, '--' ]
4383
4384 if args:
4385 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004386 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004387 diff_cmd.append(arg)
4388 else:
4389 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004390
4391 return diff_cmd
4392
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004393def MatchingFileType(file_name, extensions):
4394 """Returns true if the file name ends with one of the given extensions."""
4395 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004396
enne@chromium.org555cfe42014-01-29 18:21:39 +00004397@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004398def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004399 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004400 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004401 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004402 parser.add_option('--full', action='store_true',
4403 help='Reformat the full content of all touched files')
4404 parser.add_option('--dry-run', action='store_true',
4405 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004406 parser.add_option('--python', action='store_true',
4407 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004408 parser.add_option('--diff', action='store_true',
4409 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004410 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004411
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004412 # git diff generates paths against the root of the repository. Change
4413 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004414 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004415 if rel_base_path:
4416 os.chdir(rel_base_path)
4417
digit@chromium.org29e47272013-05-17 17:01:46 +00004418 # Grab the merge-base commit, i.e. the upstream commit of the current
4419 # branch when it was created or the last time it was rebased. This is
4420 # to cover the case where the user may have called "git fetch origin",
4421 # moving the origin branch to a newer commit, but hasn't rebased yet.
4422 upstream_commit = None
4423 cl = Changelist()
4424 upstream_branch = cl.GetUpstreamBranch()
4425 if upstream_branch:
4426 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4427 upstream_commit = upstream_commit.strip()
4428
4429 if not upstream_commit:
4430 DieWithError('Could not find base commit for this branch. '
4431 'Are you in detached state?')
4432
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004433 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4434 diff_output = RunGit(changed_files_cmd)
4435 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004436 # Filter out files deleted by this CL
4437 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004438
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004439 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4440 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4441 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004442 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004443
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004444 top_dir = os.path.normpath(
4445 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4446
4447 # Locate the clang-format binary in the checkout
4448 try:
4449 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4450 except clang_format.NotFoundError, e:
4451 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004452
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004453 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4454 # formatted. This is used to block during the presubmit.
4455 return_value = 0
4456
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004457 if clang_diff_files:
4458 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004459 cmd = [clang_format_tool]
4460 if not opts.dry_run and not opts.diff:
4461 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004462 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004463 if opts.diff:
4464 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004465 else:
4466 env = os.environ.copy()
4467 env['PATH'] = str(os.path.dirname(clang_format_tool))
4468 try:
4469 script = clang_format.FindClangFormatScriptInChromiumTree(
4470 'clang-format-diff.py')
4471 except clang_format.NotFoundError, e:
4472 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004473
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004474 cmd = [sys.executable, script, '-p0']
4475 if not opts.dry_run and not opts.diff:
4476 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004477
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004478 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4479 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004480
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004481 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4482 if opts.diff:
4483 sys.stdout.write(stdout)
4484 if opts.dry_run and len(stdout) > 0:
4485 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004486
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004487 # Similar code to above, but using yapf on .py files rather than clang-format
4488 # on C/C++ files
4489 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004490 yapf_tool = gclient_utils.FindExecutable('yapf')
4491 if yapf_tool is None:
4492 DieWithError('yapf not found in PATH')
4493
4494 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004495 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004496 cmd = [yapf_tool]
4497 if not opts.dry_run and not opts.diff:
4498 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004499 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004500 if opts.diff:
4501 sys.stdout.write(stdout)
4502 else:
4503 # TODO(sbc): yapf --lines mode still has some issues.
4504 # https://github.com/google/yapf/issues/154
4505 DieWithError('--python currently only works with --full')
4506
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004507 # Dart's formatter does not have the nice property of only operating on
4508 # modified chunks, so hard code full.
4509 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004510 try:
4511 command = [dart_format.FindDartFmtToolInChromiumTree()]
4512 if not opts.dry_run and not opts.diff:
4513 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004514 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004515
ppi@chromium.org6593d932016-03-03 15:41:15 +00004516 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004517 if opts.dry_run and stdout:
4518 return_value = 2
4519 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004520 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4521 'found in this checkout. Files in other languages are still ' +
4522 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004523
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004524 # Format GN build files. Always run on full build files for canonical form.
4525 if gn_diff_files:
4526 cmd = ['gn', 'format']
4527 if not opts.dry_run and not opts.diff:
4528 cmd.append('--in-place')
4529 for gn_diff_file in gn_diff_files:
4530 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4531 if opts.diff:
4532 sys.stdout.write(stdout)
4533
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004534 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004535
4536
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004537@subcommand.usage('<codereview url or issue id>')
4538def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004539 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004540 _, args = parser.parse_args(args)
4541
4542 if len(args) != 1:
4543 parser.print_help()
4544 return 1
4545
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004546 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004547 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004548 parser.print_help()
4549 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004550 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004551
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004552 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004553 output = RunGit(['config', '--local', '--get-regexp',
4554 r'branch\..*\.%s' % issueprefix],
4555 error_ok=True)
4556 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004557 if issue == target_issue:
4558 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004559
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004560 branches = []
4561 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4562 branches.extend(find_issues(cls.IssueSettingPrefix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004563 if len(branches) == 0:
4564 print 'No branch found for issue %s.' % target_issue
4565 return 1
4566 if len(branches) == 1:
4567 RunGit(['checkout', branches[0]])
4568 else:
4569 print 'Multiple branches match issue %s:' % target_issue
4570 for i in range(len(branches)):
4571 print '%d: %s' % (i, branches[i])
4572 which = raw_input('Choose by index: ')
4573 try:
4574 RunGit(['checkout', branches[int(which)]])
4575 except (IndexError, ValueError):
4576 print 'Invalid selection, not checking out any branch.'
4577 return 1
4578
4579 return 0
4580
4581
maruel@chromium.org29404b52014-09-08 22:58:00 +00004582def CMDlol(parser, args):
4583 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004584 print zlib.decompress(base64.b64decode(
4585 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4586 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4587 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4588 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004589 return 0
4590
4591
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004592class OptionParser(optparse.OptionParser):
4593 """Creates the option parse and add --verbose support."""
4594 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004595 optparse.OptionParser.__init__(
4596 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004597 self.add_option(
4598 '-v', '--verbose', action='count', default=0,
4599 help='Use 2 times for more debugging info')
4600
4601 def parse_args(self, args=None, values=None):
4602 options, args = optparse.OptionParser.parse_args(self, args, values)
4603 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4604 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4605 return options, args
4606
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004607
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004608def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004609 if sys.hexversion < 0x02060000:
4610 print >> sys.stderr, (
4611 '\nYour python version %s is unsupported, please upgrade.\n' %
4612 sys.version.split(' ', 1)[0])
4613 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004614
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004615 # Reload settings.
4616 global settings
4617 settings = Settings()
4618
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004619 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004620 dispatcher = subcommand.CommandDispatcher(__name__)
4621 try:
4622 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004623 except auth.AuthenticationError as e:
4624 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004625 except urllib2.HTTPError, e:
4626 if e.code != 500:
4627 raise
4628 DieWithError(
4629 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4630 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004631 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004632
4633
4634if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004635 # These affect sys.stdout so do it outside of main() to simplify mocks in
4636 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004637 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004638 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004639 try:
4640 sys.exit(main(sys.argv[1:]))
4641 except KeyboardInterrupt:
4642 sys.stderr.write('interrupted\n')
4643 sys.exit(1)