blob: 49891fcf5c8af98c55f830f6292bd018b23573d9 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000838class _ParsedIssueNumberArgument(object):
839 def __init__(self, issue=None, patchset=None, hostname=None):
840 self.issue = issue
841 self.patchset = patchset
842 self.hostname = hostname
843
844 @property
845 def valid(self):
846 return self.issue is not None
847
848
849class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
850 def __init__(self, *args, **kwargs):
851 self.patch_url = kwargs.pop('patch_url', None)
852 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
853
854
855def ParseIssueNumberArgument(arg):
856 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
857 fail_result = _ParsedIssueNumberArgument()
858
859 if arg.isdigit():
860 return _ParsedIssueNumberArgument(issue=int(arg))
861 if not arg.startswith('http'):
862 return fail_result
863 url = gclient_utils.UpgradeToHttps(arg)
864 try:
865 parsed_url = urlparse.urlparse(url)
866 except ValueError:
867 return fail_result
868 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
869 tmp = cls.ParseIssueURL(parsed_url)
870 if tmp is not None:
871 return tmp
872 return fail_result
873
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000876 """Changelist works with one changelist in local branch.
877
878 Supports two codereview backends: Rietveld or Gerrit, selected at object
879 creation.
880
881 Not safe for concurrent multi-{thread,process} use.
882 """
883
884 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
885 """Create a new ChangeList instance.
886
887 If issue is given, the codereview must be given too.
888
889 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
890 Otherwise, it's decided based on current configuration of the local branch,
891 with default being 'rietveld' for backwards compatibility.
892 See _load_codereview_impl for more details.
893
894 **kwargs will be passed directly to codereview implementation.
895 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000897 global settings
898 if not settings:
899 # Happens when git_cl.py is used as a utility library.
900 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901
902 if issue:
903 assert codereview, 'codereview must be known, if issue is known'
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.branchref = branchref
906 if self.branchref:
907 self.branch = ShortBranchName(self.branchref)
908 else:
909 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000911 self.lookedup_issue = False
912 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.has_description = False
914 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000915 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000917 self.cc = None
918 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000922 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000924 assert self._codereview_impl
925 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 def _load_codereview_impl(self, codereview=None, **kwargs):
928 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000929 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
930 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
931 self._codereview = codereview
932 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 return
934
935 # Automatic selection based on issue number set for a current branch.
936 # Rietveld takes precedence over Gerrit.
937 assert not self.issue
938 # Whether we find issue or not, we are doing the lookup.
939 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000940 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 setting = cls.IssueSetting(self.GetBranch())
942 issue = RunGit(['config', setting], error_ok=True).strip()
943 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = cls(self, **kwargs)
946 self.issue = int(issue)
947 return
948
949 # No issue is set for this branch, so decide based on repo-wide settings.
950 return self._load_codereview_impl(
951 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
952 **kwargs)
953
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 def IsGerrit(self):
955 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000956
957 def GetCCList(self):
958 """Return the users cc'd on this CL.
959
960 Return is a string suitable for passing to gcl with the --cc flag.
961 """
962 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000963 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000964 more_cc = ','.join(self.watchers)
965 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
966 return self.cc
967
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000968 def GetCCListWithoutDefault(self):
969 """Return the users cc'd on this CL excluding default ones."""
970 if self.cc is None:
971 self.cc = ','.join(self.watchers)
972 return self.cc
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def SetWatchers(self, watchers):
975 """Set the list of email addresses that should be cc'd based on the changed
976 files in this CL.
977 """
978 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
980 def GetBranch(self):
981 """Returns the short branch name, e.g. 'master'."""
982 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000984 if not branchref:
985 return None
986 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 self.branch = ShortBranchName(self.branchref)
988 return self.branch
989
990 def GetBranchRef(self):
991 """Returns the full branch name, e.g. 'refs/heads/master'."""
992 self.GetBranch() # Poke the lazy loader.
993 return self.branchref
994
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000995 @staticmethod
996 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000997 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 e.g. 'origin', 'refs/heads/master'
999 """
1000 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1002 error_ok=True).strip()
1003 if upstream_branch:
1004 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1005 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001006 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1007 error_ok=True).strip()
1008 if upstream_branch:
1009 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001011 # Fall back on trying a git-svn upstream branch.
1012 if settings.GetIsGitSvn():
1013 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Else, try to guess the origin remote.
1016 remote_branches = RunGit(['branch', '-r']).split()
1017 if 'origin/master' in remote_branches:
1018 # Fall back on origin/master if it exits.
1019 remote = 'origin'
1020 upstream_branch = 'refs/heads/master'
1021 elif 'origin/trunk' in remote_branches:
1022 # Fall back on origin/trunk if it exists. Generally a shared
1023 # git-svn clone
1024 remote = 'origin'
1025 upstream_branch = 'refs/heads/trunk'
1026 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027 DieWithError(
1028 'Unable to determine default branch to diff against.\n'
1029 'Either pass complete "git diff"-style arguments, like\n'
1030 ' git cl upload origin/master\n'
1031 'or verify this branch is set up to track another \n'
1032 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033
1034 return remote, upstream_branch
1035
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001036 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001037 upstream_branch = self.GetUpstreamBranch()
1038 if not BranchExists(upstream_branch):
1039 DieWithError('The upstream for the current branch (%s) does not exist '
1040 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001041 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001042 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 def GetUpstreamBranch(self):
1045 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001046 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001048 upstream_branch = upstream_branch.replace('refs/heads/',
1049 'refs/remotes/%s/' % remote)
1050 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1051 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052 self.upstream_branch = upstream_branch
1053 return self.upstream_branch
1054
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001055 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001056 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001057 remote, branch = None, self.GetBranch()
1058 seen_branches = set()
1059 while branch not in seen_branches:
1060 seen_branches.add(branch)
1061 remote, branch = self.FetchUpstreamTuple(branch)
1062 branch = ShortBranchName(branch)
1063 if remote != '.' or branch.startswith('refs/remotes'):
1064 break
1065 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001066 remotes = RunGit(['remote'], error_ok=True).split()
1067 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001068 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001069 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001070 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001071 logging.warning('Could not determine which remote this change is '
1072 'associated with, so defaulting to "%s". This may '
1073 'not be what you want. You may prevent this message '
1074 'by running "git svn info" as documented here: %s',
1075 self._remote,
1076 GIT_INSTRUCTIONS_URL)
1077 else:
1078 logging.warn('Could not determine which remote this change is '
1079 'associated with. You may prevent this message by '
1080 'running "git svn info" as documented here: %s',
1081 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001082 branch = 'HEAD'
1083 if branch.startswith('refs/remotes'):
1084 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001085 elif branch.startswith('refs/branch-heads/'):
1086 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001087 else:
1088 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001089 return self._remote
1090
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 def GitSanityChecks(self, upstream_git_obj):
1092 """Checks git repo status and ensures diff is from local commits."""
1093
sbc@chromium.org79706062015-01-14 21:18:12 +00001094 if upstream_git_obj is None:
1095 if self.GetBranch() is None:
1096 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001097 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 else:
1099 print >> sys.stderr, (
1100 'ERROR: no upstream branch')
1101 return False
1102
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001103 # Verify the commit we're diffing against is in our current branch.
1104 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1105 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1106 if upstream_sha != common_ancestor:
1107 print >> sys.stderr, (
1108 'ERROR: %s is not in the current branch. You may need to rebase '
1109 'your tracking branch' % upstream_sha)
1110 return False
1111
1112 # List the commits inside the diff, and verify they are all local.
1113 commits_in_diff = RunGit(
1114 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1115 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1116 remote_branch = remote_branch.strip()
1117 if code != 0:
1118 _, remote_branch = self.GetRemoteBranch()
1119
1120 commits_in_remote = RunGit(
1121 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1122
1123 common_commits = set(commits_in_diff) & set(commits_in_remote)
1124 if common_commits:
1125 print >> sys.stderr, (
1126 'ERROR: Your diff contains %d commits already in %s.\n'
1127 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1128 'the diff. If you are using a custom git flow, you can override'
1129 ' the reference used for this check with "git config '
1130 'gitcl.remotebranch <git-ref>".' % (
1131 len(common_commits), remote_branch, upstream_git_obj))
1132 return False
1133 return True
1134
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001135 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001136 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001137
1138 Returns None if it is not set.
1139 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1141 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001142
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001143 def GetGitSvnRemoteUrl(self):
1144 """Return the configured git-svn remote URL parsed from git svn info.
1145
1146 Returns None if it is not set.
1147 """
1148 # URL is dependent on the current directory.
1149 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1150 if data:
1151 keys = dict(line.split(': ', 1) for line in data.splitlines()
1152 if ': ' in line)
1153 return keys.get('URL', None)
1154 return None
1155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 def GetRemoteUrl(self):
1157 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1158
1159 Returns None if there is no remote.
1160 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001161 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001162 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1163
1164 # If URL is pointing to a local directory, it is probably a git cache.
1165 if os.path.isdir(url):
1166 url = RunGit(['config', 'remote.%s.url' % remote],
1167 error_ok=True,
1168 cwd=url).strip()
1169 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001171 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001172 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001173 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174 issue = RunGit(['config',
1175 self._codereview_impl.IssueSetting(self.GetBranch())],
1176 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001177 self.issue = int(issue) or None if issue else None
1178 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179 return self.issue
1180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 def GetIssueURL(self):
1182 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001183 issue = self.GetIssue()
1184 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001185 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001186 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
1188 def GetDescription(self, pretty=False):
1189 if not self.has_description:
1190 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001191 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 self.has_description = True
1193 if pretty:
1194 wrapper = textwrap.TextWrapper()
1195 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1196 return wrapper.fill(self.description)
1197 return self.description
1198
1199 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001200 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001201 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001203 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001204 self.patchset = int(patchset) or None if patchset else None
1205 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206 return self.patchset
1207
1208 def SetPatchset(self, patchset):
1209 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001210 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001212 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001213 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001215 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001216 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001219 def SetIssue(self, issue=None):
1220 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001221 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1222 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 RunGit(['config', issue_setting, str(issue)])
1226 codereview_server = self._codereview_impl.GetCodereviewServer()
1227 if codereview_server:
1228 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001230 current_issue = self.GetIssue()
1231 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233 self.issue = None
1234 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001236 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001237 if not self.GitSanityChecks(upstream_branch):
1238 DieWithError('\nGit sanity check failure')
1239
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001240 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001241 if not root:
1242 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001243 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001244
1245 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001246 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001248 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001249 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001250 except subprocess2.CalledProcessError:
1251 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001252 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001253 'This branch probably doesn\'t exist anymore. To reset the\n'
1254 'tracking branch, please run\n'
1255 ' git branch --set-upstream %s trunk\n'
1256 'replacing trunk with origin/master or the relevant branch') %
1257 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001258
maruel@chromium.org52424302012-08-29 15:14:30 +00001259 issue = self.GetIssue()
1260 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001261 if issue:
1262 description = self.GetDescription()
1263 else:
1264 # If the change was never uploaded, use the log messages of all commits
1265 # up to the branch point, as git cl upload will prefill the description
1266 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001267 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1268 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001269
1270 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001271 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001272 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001273 name,
1274 description,
1275 absroot,
1276 files,
1277 issue,
1278 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001279 author,
1280 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001281
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001282 def UpdateDescription(self, description):
1283 self.description = description
1284 return self._codereview_impl.UpdateDescriptionRemote(description)
1285
1286 def RunHook(self, committing, may_prompt, verbose, change):
1287 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1288 try:
1289 return presubmit_support.DoPresubmitChecks(change, committing,
1290 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1291 default_presubmit=None, may_prompt=may_prompt,
1292 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1293 except presubmit_support.PresubmitFailure, e:
1294 DieWithError(
1295 ('%s\nMaybe your depot_tools is out of date?\n'
1296 'If all fails, contact maruel@') % e)
1297
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001298 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1299 """Fetches and applies the issue patch from codereview to local branch."""
1300 if issue_arg.isdigit():
1301 parsed_issue_arg = _RietveldParsedIssueNumberArgument(int(issue_arg))
1302 else:
1303 # Assume url.
1304 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1305 urlparse.urlparse(issue_arg))
1306 if not parsed_issue_arg or not parsed_issue_arg.valid:
1307 DieWithError('Failed to parse issue argument "%s". '
1308 'Must be an issue number or a valid URL.' % issue_arg)
1309 return self._codereview_impl.CMDPatchWithParsedIssue(
1310 parsed_issue_arg, reject, nocommit, directory)
1311
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001312 # Forward methods to codereview specific implementation.
1313
1314 def CloseIssue(self):
1315 return self._codereview_impl.CloseIssue()
1316
1317 def GetStatus(self):
1318 return self._codereview_impl.GetStatus()
1319
1320 def GetCodereviewServer(self):
1321 return self._codereview_impl.GetCodereviewServer()
1322
1323 def GetApprovingReviewers(self):
1324 return self._codereview_impl.GetApprovingReviewers()
1325
1326 def GetMostRecentPatchset(self):
1327 return self._codereview_impl.GetMostRecentPatchset()
1328
1329 def __getattr__(self, attr):
1330 # This is because lots of untested code accesses Rietveld-specific stuff
1331 # directly, and it's hard to fix for sure. So, just let it work, and fix
1332 # on a cases by case basis.
1333 return getattr(self._codereview_impl, attr)
1334
1335
1336class _ChangelistCodereviewBase(object):
1337 """Abstract base class encapsulating codereview specifics of a changelist."""
1338 def __init__(self, changelist):
1339 self._changelist = changelist # instance of Changelist
1340
1341 def __getattr__(self, attr):
1342 # Forward methods to changelist.
1343 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1344 # _RietveldChangelistImpl to avoid this hack?
1345 return getattr(self._changelist, attr)
1346
1347 def GetStatus(self):
1348 """Apply a rough heuristic to give a simple summary of an issue's review
1349 or CQ status, assuming adherence to a common workflow.
1350
1351 Returns None if no issue for this branch, or specific string keywords.
1352 """
1353 raise NotImplementedError()
1354
1355 def GetCodereviewServer(self):
1356 """Returns server URL without end slash, like "https://codereview.com"."""
1357 raise NotImplementedError()
1358
1359 def FetchDescription(self):
1360 """Fetches and returns description from the codereview server."""
1361 raise NotImplementedError()
1362
1363 def GetCodereviewServerSetting(self):
1364 """Returns git config setting for the codereview server."""
1365 raise NotImplementedError()
1366
1367 @staticmethod
1368 def IssueSetting(branch):
1369 """Returns name of git config setting which stores issue number for a given
1370 branch."""
1371 raise NotImplementedError()
1372
1373 def PatchsetSetting(self):
1374 """Returns name of git config setting which stores issue number."""
1375 raise NotImplementedError()
1376
1377 def GetRieveldObjForPresubmit(self):
1378 # This is an unfortunate Rietveld-embeddedness in presubmit.
1379 # For non-Rietveld codereviews, this probably should return a dummy object.
1380 raise NotImplementedError()
1381
1382 def UpdateDescriptionRemote(self, description):
1383 """Update the description on codereview site."""
1384 raise NotImplementedError()
1385
1386 def CloseIssue(self):
1387 """Closes the issue."""
1388 raise NotImplementedError()
1389
1390 def GetApprovingReviewers(self):
1391 """Returns a list of reviewers approving the change.
1392
1393 Note: not necessarily committers.
1394 """
1395 raise NotImplementedError()
1396
1397 def GetMostRecentPatchset(self):
1398 """Returns the most recent patchset number from the codereview site."""
1399 raise NotImplementedError()
1400
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001401 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1402 directory):
1403 """Fetches and applies the issue.
1404
1405 Arguments:
1406 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1407 reject: if True, reject the failed patch instead of switching to 3-way
1408 merge. Rietveld only.
1409 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1410 only.
1411 directory: switch to directory before applying the patch. Rietveld only.
1412 """
1413 raise NotImplementedError()
1414
1415 @staticmethod
1416 def ParseIssueURL(parsed_url):
1417 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1418 failed."""
1419 raise NotImplementedError()
1420
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421
1422class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1423 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1424 super(_RietveldChangelistImpl, self).__init__(changelist)
1425 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1426 settings.GetDefaultServerUrl()
1427
1428 self._rietveld_server = rietveld_server
1429 self._auth_config = auth_config
1430 self._props = None
1431 self._rpc_server = None
1432
1433 def GetAuthConfig(self):
1434 return self._auth_config
1435
1436 def GetCodereviewServer(self):
1437 if not self._rietveld_server:
1438 # If we're on a branch then get the server potentially associated
1439 # with that branch.
1440 if self.GetIssue():
1441 rietveld_server_setting = self.GetCodereviewServerSetting()
1442 if rietveld_server_setting:
1443 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1444 ['config', rietveld_server_setting], error_ok=True).strip())
1445 if not self._rietveld_server:
1446 self._rietveld_server = settings.GetDefaultServerUrl()
1447 return self._rietveld_server
1448
1449 def FetchDescription(self):
1450 issue = self.GetIssue()
1451 assert issue
1452 try:
1453 return self.RpcServer().get_description(issue).strip()
1454 except urllib2.HTTPError as e:
1455 if e.code == 404:
1456 DieWithError(
1457 ('\nWhile fetching the description for issue %d, received a '
1458 '404 (not found)\n'
1459 'error. It is likely that you deleted this '
1460 'issue on the server. If this is the\n'
1461 'case, please run\n\n'
1462 ' git cl issue 0\n\n'
1463 'to clear the association with the deleted issue. Then run '
1464 'this command again.') % issue)
1465 else:
1466 DieWithError(
1467 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1468 except urllib2.URLError as e:
1469 print >> sys.stderr, (
1470 'Warning: Failed to retrieve CL description due to network '
1471 'failure.')
1472 return ''
1473
1474 def GetMostRecentPatchset(self):
1475 return self.GetIssueProperties()['patchsets'][-1]
1476
1477 def GetPatchSetDiff(self, issue, patchset):
1478 return self.RpcServer().get(
1479 '/download/issue%s_%s.diff' % (issue, patchset))
1480
1481 def GetIssueProperties(self):
1482 if self._props is None:
1483 issue = self.GetIssue()
1484 if not issue:
1485 self._props = {}
1486 else:
1487 self._props = self.RpcServer().get_issue_properties(issue, True)
1488 return self._props
1489
1490 def GetApprovingReviewers(self):
1491 return get_approving_reviewers(self.GetIssueProperties())
1492
1493 def AddComment(self, message):
1494 return self.RpcServer().add_comment(self.GetIssue(), message)
1495
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001496 def GetStatus(self):
1497 """Apply a rough heuristic to give a simple summary of an issue's review
1498 or CQ status, assuming adherence to a common workflow.
1499
1500 Returns None if no issue for this branch, or one of the following keywords:
1501 * 'error' - error from review tool (including deleted issues)
1502 * 'unsent' - not sent for review
1503 * 'waiting' - waiting for review
1504 * 'reply' - waiting for owner to reply to review
1505 * 'lgtm' - LGTM from at least one approved reviewer
1506 * 'commit' - in the commit queue
1507 * 'closed' - closed
1508 """
1509 if not self.GetIssue():
1510 return None
1511
1512 try:
1513 props = self.GetIssueProperties()
1514 except urllib2.HTTPError:
1515 return 'error'
1516
1517 if props.get('closed'):
1518 # Issue is closed.
1519 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001520 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001521 # Issue is in the commit queue.
1522 return 'commit'
1523
1524 try:
1525 reviewers = self.GetApprovingReviewers()
1526 except urllib2.HTTPError:
1527 return 'error'
1528
1529 if reviewers:
1530 # Was LGTM'ed.
1531 return 'lgtm'
1532
1533 messages = props.get('messages') or []
1534
1535 if not messages:
1536 # No message was sent.
1537 return 'unsent'
1538 if messages[-1]['sender'] != props.get('owner_email'):
1539 # Non-LGTM reply from non-owner
1540 return 'reply'
1541 return 'waiting'
1542
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001543 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001544 return self.RpcServer().update_description(
1545 self.GetIssue(), self.description)
1546
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001548 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001549
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001550 def SetFlag(self, flag, value):
1551 """Patchset must match."""
1552 if not self.GetPatchset():
1553 DieWithError('The patchset needs to match. Send another patchset.')
1554 try:
1555 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001556 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001557 except urllib2.HTTPError, e:
1558 if e.code == 404:
1559 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1560 if e.code == 403:
1561 DieWithError(
1562 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1563 'match?') % (self.GetIssue(), self.GetPatchset()))
1564 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001566 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567 """Returns an upload.RpcServer() to access this review's rietveld instance.
1568 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001569 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001570 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001571 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001572 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001573 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001574
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575 @staticmethod
1576 def IssueSetting(branch):
1577 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 """Return the git setting that stores this change's most recent patchset."""
1581 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001583 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001585 branch = self.GetBranch()
1586 if branch:
1587 return 'branch.%s.rietveldserver' % branch
1588 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590 def GetRieveldObjForPresubmit(self):
1591 return self.RpcServer()
1592
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001593 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1594 directory):
1595 # TODO(maruel): Use apply_issue.py
1596
1597 # PatchIssue should never be called with a dirty tree. It is up to the
1598 # caller to check this, but just in case we assert here since the
1599 # consequences of the caller not checking this could be dire.
1600 assert(not git_common.is_dirty_git_tree('apply'))
1601 assert(parsed_issue_arg.valid)
1602 self._changelist.issue = parsed_issue_arg.issue
1603 if parsed_issue_arg.hostname:
1604 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1605
1606 if parsed_issue_arg.patch_url:
1607 assert parsed_issue_arg.patchset
1608 patchset = parsed_issue_arg.patchset
1609 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1610 else:
1611 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1612 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1613
1614 # Switch up to the top-level directory, if necessary, in preparation for
1615 # applying the patch.
1616 top = settings.GetRelativeRoot()
1617 if top:
1618 os.chdir(top)
1619
1620 # Git patches have a/ at the beginning of source paths. We strip that out
1621 # with a sed script rather than the -p flag to patch so we can feed either
1622 # Git or svn-style patches into the same apply command.
1623 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1624 try:
1625 patch_data = subprocess2.check_output(
1626 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1627 except subprocess2.CalledProcessError:
1628 DieWithError('Git patch mungling failed.')
1629 logging.info(patch_data)
1630
1631 # We use "git apply" to apply the patch instead of "patch" so that we can
1632 # pick up file adds.
1633 # The --index flag means: also insert into the index (so we catch adds).
1634 cmd = ['git', 'apply', '--index', '-p0']
1635 if directory:
1636 cmd.extend(('--directory', directory))
1637 if reject:
1638 cmd.append('--reject')
1639 elif IsGitVersionAtLeast('1.7.12'):
1640 cmd.append('--3way')
1641 try:
1642 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1643 stdin=patch_data, stdout=subprocess2.VOID)
1644 except subprocess2.CalledProcessError:
1645 print 'Failed to apply the patch'
1646 return 1
1647
1648 # If we had an issue, commit the current state and register the issue.
1649 if not nocommit:
1650 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1651 'patch from issue %(i)s at patchset '
1652 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1653 % {'i': self.GetIssue(), 'p': patchset})])
1654 self.SetIssue(self.GetIssue())
1655 self.SetPatchset(patchset)
1656 print "Committed patch locally."
1657 else:
1658 print "Patch applied to index."
1659 return 0
1660
1661 @staticmethod
1662 def ParseIssueURL(parsed_url):
1663 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1664 return None
1665 # Typical url: https://domain/<issue_number>[/[other]]
1666 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1667 if match:
1668 return _RietveldParsedIssueNumberArgument(
1669 issue=int(match.group(1)),
1670 hostname=parsed_url.netloc)
1671 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1672 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1673 if match:
1674 return _RietveldParsedIssueNumberArgument(
1675 issue=int(match.group(1)),
1676 patchset=int(match.group(2)),
1677 hostname=parsed_url.netloc,
1678 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1679 return None
1680
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001681
1682class _GerritChangelistImpl(_ChangelistCodereviewBase):
1683 def __init__(self, changelist, auth_config=None):
1684 # auth_config is Rietveld thing, kept here to preserve interface only.
1685 super(_GerritChangelistImpl, self).__init__(changelist)
1686 self._change_id = None
1687 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1688 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1689
1690 def _GetGerritHost(self):
1691 # Lazy load of configs.
1692 self.GetCodereviewServer()
1693 return self._gerrit_host
1694
1695 def GetCodereviewServer(self):
1696 if not self._gerrit_server:
1697 # If we're on a branch then get the server potentially associated
1698 # with that branch.
1699 if self.GetIssue():
1700 gerrit_server_setting = self.GetCodereviewServerSetting()
1701 if gerrit_server_setting:
1702 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1703 error_ok=True).strip()
1704 if self._gerrit_server:
1705 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1706 if not self._gerrit_server:
1707 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1708 # has "-review" suffix for lowest level subdomain.
1709 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1710 parts[0] = parts[0] + '-review'
1711 self._gerrit_host = '.'.join(parts)
1712 self._gerrit_server = 'https://%s' % self._gerrit_host
1713 return self._gerrit_server
1714
1715 @staticmethod
1716 def IssueSetting(branch):
1717 return 'branch.%s.gerritissue' % branch
1718
1719 def PatchsetSetting(self):
1720 """Return the git setting that stores this change's most recent patchset."""
1721 return 'branch.%s.gerritpatchset' % self.GetBranch()
1722
1723 def GetCodereviewServerSetting(self):
1724 """Returns the git setting that stores this change's Gerrit server."""
1725 branch = self.GetBranch()
1726 if branch:
1727 return 'branch.%s.gerritserver' % branch
1728 return None
1729
1730 def GetRieveldObjForPresubmit(self):
1731 class ThisIsNotRietveldIssue(object):
1732 def __nonzero__(self):
1733 # This is a hack to make presubmit_support think that rietveld is not
1734 # defined, yet still ensure that calls directly result in a decent
1735 # exception message below.
1736 return False
1737
1738 def __getattr__(self, attr):
1739 print(
1740 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1741 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1742 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1743 'or use Rietveld for codereview.\n'
1744 'See also http://crbug.com/579160.' % attr)
1745 raise NotImplementedError()
1746 return ThisIsNotRietveldIssue()
1747
1748 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001749 """Apply a rough heuristic to give a simple summary of an issue's review
1750 or CQ status, assuming adherence to a common workflow.
1751
1752 Returns None if no issue for this branch, or one of the following keywords:
1753 * 'error' - error from review tool (including deleted issues)
1754 * 'unsent' - no reviewers added
1755 * 'waiting' - waiting for review
1756 * 'reply' - waiting for owner to reply to review
1757 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
1758 * 'lgtm' - Code-Review +2 from at least one approved reviewer
1759 * 'commit' - in the commit queue
1760 * 'closed' - abandoned
1761 """
1762 if not self.GetIssue():
1763 return None
1764
1765 try:
1766 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
1767 except httplib.HTTPException:
1768 return 'error'
1769
1770 if data['status'] == 'ABANDONED':
1771 return 'closed'
1772
1773 cq_label = data['labels'].get('Commit-Queue', {})
1774 if cq_label:
1775 # Vote value is a stringified integer, which we expect from 0 to 2.
1776 vote_value = cq_label.get('value', '0')
1777 vote_text = cq_label.get('values', {}).get(vote_value, '')
1778 if vote_text.lower() == 'commit':
1779 return 'commit'
1780
1781 lgtm_label = data['labels'].get('Code-Review', {})
1782 if lgtm_label:
1783 if 'rejected' in lgtm_label:
1784 return 'not lgtm'
1785 if 'approved' in lgtm_label:
1786 return 'lgtm'
1787
1788 if not data.get('reviewers', {}).get('REVIEWER', []):
1789 return 'unsent'
1790
1791 messages = data.get('messages', [])
1792 if messages:
1793 owner = data['owner'].get('_account_id')
1794 last_message_author = messages[-1].get('author', {}).get('_account_id')
1795 if owner != last_message_author:
1796 # Some reply from non-owner.
1797 return 'reply'
1798
1799 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001800
1801 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001802 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001803 return data['revisions'][data['current_revision']]['_number']
1804
1805 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001806 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807 return data['revisions'][data['current_revision']]['commit_with_footers']
1808
1809 def UpdateDescriptionRemote(self, description):
1810 # TODO(tandrii)
1811 raise NotImplementedError()
1812
1813 def CloseIssue(self):
1814 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1815
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001816 def SubmitIssue(self, wait_for_merge=True):
1817 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
1818 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001820 def _GetChangeDetail(self, options):
1821 return gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1822 options)
1823
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001824 def CMDLand(self, force, bypass_hooks, verbose):
1825 if git_common.is_dirty_git_tree('land'):
1826 return 1
1827 differs = True
1828 last_upload = RunGit(['config',
1829 'branch.%s.gerritsquashhash' % self.GetBranch()],
1830 error_ok=True).strip()
1831 # Note: git diff outputs nothing if there is no diff.
1832 if not last_upload or RunGit(['diff', last_upload]).strip():
1833 print('WARNING: some changes from local branch haven\'t been uploaded')
1834 else:
1835 detail = self._GetChangeDetail(['CURRENT_REVISION'])
1836 if detail['current_revision'] == last_upload:
1837 differs = False
1838 else:
1839 print('WARNING: local branch contents differ from latest uploaded '
1840 'patchset')
1841 if differs:
1842 if not force:
1843 ask_for_data(
1844 'Do you want to submit latest Gerrit patchset and bypass hooks?')
1845 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
1846 elif not bypass_hooks:
1847 hook_results = self.RunHook(
1848 committing=True,
1849 may_prompt=not force,
1850 verbose=verbose,
1851 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
1852 if not hook_results.should_continue():
1853 return 1
1854
1855 self.SubmitIssue(wait_for_merge=True)
1856 print('Issue %s has been submitted.' % self.GetIssueURL())
1857 return 0
1858
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001859 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1860 directory):
1861 assert not reject
1862 assert not nocommit
1863 assert not directory
1864 assert parsed_issue_arg.valid
1865
1866 self._changelist.issue = parsed_issue_arg.issue
1867
1868 if parsed_issue_arg.hostname:
1869 self._gerrit_host = parsed_issue_arg.hostname
1870 self._gerrit_server = 'https://%s' % self._gerrit_host
1871
1872 detail = self._GetChangeDetail(['ALL_REVISIONS'])
1873
1874 if not parsed_issue_arg.patchset:
1875 # Use current revision by default.
1876 revision_info = detail['revisions'][detail['current_revision']]
1877 patchset = int(revision_info['_number'])
1878 else:
1879 patchset = parsed_issue_arg.patchset
1880 for revision_info in detail['revisions'].itervalues():
1881 if int(revision_info['_number']) == parsed_issue_arg.patchset:
1882 break
1883 else:
1884 DieWithError('Couldn\'t find patchset %i in issue %i' %
1885 (parsed_issue_arg.patchset, self.GetIssue()))
1886
1887 fetch_info = revision_info['fetch']['http']
1888 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
1889 RunGit(['cherry-pick', 'FETCH_HEAD'])
1890 self.SetIssue(self.GetIssue())
1891 self.SetPatchset(patchset)
1892 print('Committed patch for issue %i pathset %i locally' %
1893 (self.GetIssue(), self.GetPatchset()))
1894 return 0
1895
1896 @staticmethod
1897 def ParseIssueURL(parsed_url):
1898 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1899 return None
1900 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
1901 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
1902 # Short urls like https://domain/<issue_number> can be used, but don't allow
1903 # specifying the patchset (you'd 404), but we allow that here.
1904 if parsed_url.path == '/':
1905 part = parsed_url.fragment
1906 else:
1907 part = parsed_url.path
1908 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
1909 if match:
1910 return _ParsedIssueNumberArgument(
1911 issue=int(match.group(2)),
1912 patchset=int(match.group(4)) if match.group(4) else None,
1913 hostname=parsed_url.netloc)
1914 return None
1915
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001916
1917_CODEREVIEW_IMPLEMENTATIONS = {
1918 'rietveld': _RietveldChangelistImpl,
1919 'gerrit': _GerritChangelistImpl,
1920}
1921
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001922
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001923class ChangeDescription(object):
1924 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001925 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001926 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001927
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001928 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001929 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001930
agable@chromium.org42c20792013-09-12 17:34:49 +00001931 @property # www.logilab.org/ticket/89786
1932 def description(self): # pylint: disable=E0202
1933 return '\n'.join(self._description_lines)
1934
1935 def set_description(self, desc):
1936 if isinstance(desc, basestring):
1937 lines = desc.splitlines()
1938 else:
1939 lines = [line.rstrip() for line in desc]
1940 while lines and not lines[0]:
1941 lines.pop(0)
1942 while lines and not lines[-1]:
1943 lines.pop(-1)
1944 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001945
piman@chromium.org336f9122014-09-04 02:16:55 +00001946 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001947 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001948 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001949 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001950 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001951 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001952
agable@chromium.org42c20792013-09-12 17:34:49 +00001953 # Get the set of R= and TBR= lines and remove them from the desciption.
1954 regexp = re.compile(self.R_LINE)
1955 matches = [regexp.match(line) for line in self._description_lines]
1956 new_desc = [l for i, l in enumerate(self._description_lines)
1957 if not matches[i]]
1958 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001959
agable@chromium.org42c20792013-09-12 17:34:49 +00001960 # Construct new unified R= and TBR= lines.
1961 r_names = []
1962 tbr_names = []
1963 for match in matches:
1964 if not match:
1965 continue
1966 people = cleanup_list([match.group(2).strip()])
1967 if match.group(1) == 'TBR':
1968 tbr_names.extend(people)
1969 else:
1970 r_names.extend(people)
1971 for name in r_names:
1972 if name not in reviewers:
1973 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001974 if add_owners_tbr:
1975 owners_db = owners.Database(change.RepositoryRoot(),
1976 fopen=file, os_path=os.path, glob=glob.glob)
1977 all_reviewers = set(tbr_names + reviewers)
1978 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1979 all_reviewers)
1980 tbr_names.extend(owners_db.reviewers_for(missing_files,
1981 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001982 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1983 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1984
1985 # Put the new lines in the description where the old first R= line was.
1986 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1987 if 0 <= line_loc < len(self._description_lines):
1988 if new_tbr_line:
1989 self._description_lines.insert(line_loc, new_tbr_line)
1990 if new_r_line:
1991 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001992 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001993 if new_r_line:
1994 self.append_footer(new_r_line)
1995 if new_tbr_line:
1996 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001997
1998 def prompt(self):
1999 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002000 self.set_description([
2001 '# Enter a description of the change.',
2002 '# This will be displayed on the codereview site.',
2003 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002004 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002005 '--------------------',
2006 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002007
agable@chromium.org42c20792013-09-12 17:34:49 +00002008 regexp = re.compile(self.BUG_LINE)
2009 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002010 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002011 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002012 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002013 if not content:
2014 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002015 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002016
2017 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002018 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2019 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002020 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002021 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002022
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002023 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002024 if self._description_lines:
2025 # Add an empty line if either the last line or the new line isn't a tag.
2026 last_line = self._description_lines[-1]
2027 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2028 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2029 self._description_lines.append('')
2030 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002031
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002032 def get_reviewers(self):
2033 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002034 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2035 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002036 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002037
2038
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002039def get_approving_reviewers(props):
2040 """Retrieves the reviewers that approved a CL from the issue properties with
2041 messages.
2042
2043 Note that the list may contain reviewers that are not committer, thus are not
2044 considered by the CQ.
2045 """
2046 return sorted(
2047 set(
2048 message['sender']
2049 for message in props['messages']
2050 if message['approval'] and message['sender'] in props['reviewers']
2051 )
2052 )
2053
2054
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055def FindCodereviewSettingsFile(filename='codereview.settings'):
2056 """Finds the given file starting in the cwd and going up.
2057
2058 Only looks up to the top of the repository unless an
2059 'inherit-review-settings-ok' file exists in the root of the repository.
2060 """
2061 inherit_ok_file = 'inherit-review-settings-ok'
2062 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002063 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2065 root = '/'
2066 while True:
2067 if filename in os.listdir(cwd):
2068 if os.path.isfile(os.path.join(cwd, filename)):
2069 return open(os.path.join(cwd, filename))
2070 if cwd == root:
2071 break
2072 cwd = os.path.dirname(cwd)
2073
2074
2075def LoadCodereviewSettingsFromFile(fileobj):
2076 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002077 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002078
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079 def SetProperty(name, setting, unset_error_ok=False):
2080 fullname = 'rietveld.' + name
2081 if setting in keyvals:
2082 RunGit(['config', fullname, keyvals[setting]])
2083 else:
2084 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2085
2086 SetProperty('server', 'CODE_REVIEW_SERVER')
2087 # Only server setting is required. Other settings can be absent.
2088 # In that case, we ignore errors raised during option deletion attempt.
2089 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002090 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2092 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002093 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002094 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002095 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2096 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002097 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002098 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002099 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002100 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2101 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002103 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002104 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002105
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002106 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2107 RunGit(['config', 'gerrit.squash-uploads',
2108 keyvals['GERRIT_SQUASH_UPLOADS']])
2109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002110 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2111 #should be of the form
2112 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2113 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2114 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2115 keyvals['ORIGIN_URL_CONFIG']])
2116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002118def urlretrieve(source, destination):
2119 """urllib is broken for SSL connections via a proxy therefore we
2120 can't use urllib.urlretrieve()."""
2121 with open(destination, 'w') as f:
2122 f.write(urllib2.urlopen(source).read())
2123
2124
ukai@chromium.org712d6102013-11-27 00:52:58 +00002125def hasSheBang(fname):
2126 """Checks fname is a #! script."""
2127 with open(fname) as f:
2128 return f.read(2).startswith('#!')
2129
2130
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002131def DownloadGerritHook(force):
2132 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002133
2134 Args:
2135 force: True to update hooks. False to install hooks if not present.
2136 """
2137 if not settings.GetIsGerrit():
2138 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002139 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002140 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2141 if not os.access(dst, os.X_OK):
2142 if os.path.exists(dst):
2143 if not force:
2144 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002145 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002146 print(
2147 'WARNING: installing Gerrit commit-msg hook.\n'
2148 ' This behavior of git cl will soon be disabled.\n'
2149 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002150 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002151 if not hasSheBang(dst):
2152 DieWithError('Not a script: %s\n'
2153 'You need to download from\n%s\n'
2154 'into .git/hooks/commit-msg and '
2155 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002156 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2157 except Exception:
2158 if os.path.exists(dst):
2159 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002160 DieWithError('\nFailed to download hooks.\n'
2161 'You need to download from\n%s\n'
2162 'into .git/hooks/commit-msg and '
2163 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002164
2165
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002166
2167def GetRietveldCodereviewSettingsInteractively():
2168 """Prompt the user for settings."""
2169 server = settings.GetDefaultServerUrl(error_ok=True)
2170 prompt = 'Rietveld server (host[:port])'
2171 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2172 newserver = ask_for_data(prompt + ':')
2173 if not server and not newserver:
2174 newserver = DEFAULT_SERVER
2175 if newserver:
2176 newserver = gclient_utils.UpgradeToHttps(newserver)
2177 if newserver != server:
2178 RunGit(['config', 'rietveld.server', newserver])
2179
2180 def SetProperty(initial, caption, name, is_url):
2181 prompt = caption
2182 if initial:
2183 prompt += ' ("x" to clear) [%s]' % initial
2184 new_val = ask_for_data(prompt + ':')
2185 if new_val == 'x':
2186 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2187 elif new_val:
2188 if is_url:
2189 new_val = gclient_utils.UpgradeToHttps(new_val)
2190 if new_val != initial:
2191 RunGit(['config', 'rietveld.' + name, new_val])
2192
2193 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2194 SetProperty(settings.GetDefaultPrivateFlag(),
2195 'Private flag (rietveld only)', 'private', False)
2196 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2197 'tree-status-url', False)
2198 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2199 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2200 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2201 'run-post-upload-hook', False)
2202
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002203@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002204def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002205 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002207 print('WARNING: git cl config works for Rietveld only.\n'
2208 'For Gerrit, see http://crbug.com/579160.')
2209 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002210 parser.add_option('--activate-update', action='store_true',
2211 help='activate auto-updating [rietveld] section in '
2212 '.git/config')
2213 parser.add_option('--deactivate-update', action='store_true',
2214 help='deactivate auto-updating [rietveld] section in '
2215 '.git/config')
2216 options, args = parser.parse_args(args)
2217
2218 if options.deactivate_update:
2219 RunGit(['config', 'rietveld.autoupdate', 'false'])
2220 return
2221
2222 if options.activate_update:
2223 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2224 return
2225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002226 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002227 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002228 return 0
2229
2230 url = args[0]
2231 if not url.endswith('codereview.settings'):
2232 url = os.path.join(url, 'codereview.settings')
2233
2234 # Load code review settings and download hooks (if available).
2235 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2236 return 0
2237
2238
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002239def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002240 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002241 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2242 branch = ShortBranchName(branchref)
2243 _, args = parser.parse_args(args)
2244 if not args:
2245 print("Current base-url:")
2246 return RunGit(['config', 'branch.%s.base-url' % branch],
2247 error_ok=False).strip()
2248 else:
2249 print("Setting base-url to %s" % args[0])
2250 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2251 error_ok=False).strip()
2252
2253
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002254def color_for_status(status):
2255 """Maps a Changelist status to color, for CMDstatus and other tools."""
2256 return {
2257 'unsent': Fore.RED,
2258 'waiting': Fore.BLUE,
2259 'reply': Fore.YELLOW,
2260 'lgtm': Fore.GREEN,
2261 'commit': Fore.MAGENTA,
2262 'closed': Fore.CYAN,
2263 'error': Fore.WHITE,
2264 }.get(status, Fore.WHITE)
2265
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002266def fetch_cl_status(branch, auth_config=None):
2267 """Fetches information for an issue and returns (branch, issue, status)."""
2268 cl = Changelist(branchref=branch, auth_config=auth_config)
2269 url = cl.GetIssueURL()
2270 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002271
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002272 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002273 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002274 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002275
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002276 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002277
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002278def get_cl_statuses(
2279 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002280 """Returns a blocking iterable of (branch, issue, color) for given branches.
2281
2282 If fine_grained is true, this will fetch CL statuses from the server.
2283 Otherwise, simply indicate if there's a matching url for the given branches.
2284
2285 If max_processes is specified, it is used as the maximum number of processes
2286 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2287 spawned.
2288 """
2289 # Silence upload.py otherwise it becomes unwieldly.
2290 upload.verbosity = 0
2291
2292 if fine_grained:
2293 # Process one branch synchronously to work through authentication, then
2294 # spawn processes to process all the other branches in parallel.
2295 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002296 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2297 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002298
2299 branches_to_fetch = branches[1:]
2300 pool = ThreadPool(
2301 min(max_processes, len(branches_to_fetch))
2302 if max_processes is not None
2303 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002304 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002305 yield x
2306 else:
2307 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2308 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002309 cl = Changelist(branchref=b, auth_config=auth_config)
2310 url = cl.GetIssueURL()
2311 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002312
rmistry@google.com2dd99862015-06-22 12:22:18 +00002313
2314def upload_branch_deps(cl, args):
2315 """Uploads CLs of local branches that are dependents of the current branch.
2316
2317 If the local branch dependency tree looks like:
2318 test1 -> test2.1 -> test3.1
2319 -> test3.2
2320 -> test2.2 -> test3.3
2321
2322 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2323 run on the dependent branches in this order:
2324 test2.1, test3.1, test3.2, test2.2, test3.3
2325
2326 Note: This function does not rebase your local dependent branches. Use it when
2327 you make a change to the parent branch that will not conflict with its
2328 dependent branches, and you would like their dependencies updated in
2329 Rietveld.
2330 """
2331 if git_common.is_dirty_git_tree('upload-branch-deps'):
2332 return 1
2333
2334 root_branch = cl.GetBranch()
2335 if root_branch is None:
2336 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2337 'Get on a branch!')
2338 if not cl.GetIssue() or not cl.GetPatchset():
2339 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2340 'patchset dependencies without an uploaded CL.')
2341
2342 branches = RunGit(['for-each-ref',
2343 '--format=%(refname:short) %(upstream:short)',
2344 'refs/heads'])
2345 if not branches:
2346 print('No local branches found.')
2347 return 0
2348
2349 # Create a dictionary of all local branches to the branches that are dependent
2350 # on it.
2351 tracked_to_dependents = collections.defaultdict(list)
2352 for b in branches.splitlines():
2353 tokens = b.split()
2354 if len(tokens) == 2:
2355 branch_name, tracked = tokens
2356 tracked_to_dependents[tracked].append(branch_name)
2357
2358 print
2359 print 'The dependent local branches of %s are:' % root_branch
2360 dependents = []
2361 def traverse_dependents_preorder(branch, padding=''):
2362 dependents_to_process = tracked_to_dependents.get(branch, [])
2363 padding += ' '
2364 for dependent in dependents_to_process:
2365 print '%s%s' % (padding, dependent)
2366 dependents.append(dependent)
2367 traverse_dependents_preorder(dependent, padding)
2368 traverse_dependents_preorder(root_branch)
2369 print
2370
2371 if not dependents:
2372 print 'There are no dependent local branches for %s' % root_branch
2373 return 0
2374
2375 print ('This command will checkout all dependent branches and run '
2376 '"git cl upload".')
2377 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2378
andybons@chromium.org962f9462016-02-03 20:00:42 +00002379 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002380 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002381 args.extend(['-t', 'Updated patchset dependency'])
2382
rmistry@google.com2dd99862015-06-22 12:22:18 +00002383 # Record all dependents that failed to upload.
2384 failures = {}
2385 # Go through all dependents, checkout the branch and upload.
2386 try:
2387 for dependent_branch in dependents:
2388 print
2389 print '--------------------------------------'
2390 print 'Running "git cl upload" from %s:' % dependent_branch
2391 RunGit(['checkout', '-q', dependent_branch])
2392 print
2393 try:
2394 if CMDupload(OptionParser(), args) != 0:
2395 print 'Upload failed for %s!' % dependent_branch
2396 failures[dependent_branch] = 1
2397 except: # pylint: disable=W0702
2398 failures[dependent_branch] = 1
2399 print
2400 finally:
2401 # Swap back to the original root branch.
2402 RunGit(['checkout', '-q', root_branch])
2403
2404 print
2405 print 'Upload complete for dependent branches!'
2406 for dependent_branch in dependents:
2407 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2408 print ' %s : %s' % (dependent_branch, upload_status)
2409 print
2410
2411 return 0
2412
2413
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002414def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002415 """Show status of changelists.
2416
2417 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002418 - Red not sent for review or broken
2419 - Blue waiting for review
2420 - Yellow waiting for you to reply to review
2421 - Green LGTM'ed
2422 - Magenta in the commit queue
2423 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002424
2425 Also see 'git cl comments'.
2426 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002427 parser.add_option('--field',
2428 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002429 parser.add_option('-f', '--fast', action='store_true',
2430 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002431 parser.add_option(
2432 '-j', '--maxjobs', action='store', type=int,
2433 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002434
2435 auth.add_auth_options(parser)
2436 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002437 if args:
2438 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002439 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002441 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002442 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002443 if options.field.startswith('desc'):
2444 print cl.GetDescription()
2445 elif options.field == 'id':
2446 issueid = cl.GetIssue()
2447 if issueid:
2448 print issueid
2449 elif options.field == 'patch':
2450 patchset = cl.GetPatchset()
2451 if patchset:
2452 print patchset
2453 elif options.field == 'url':
2454 url = cl.GetIssueURL()
2455 if url:
2456 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002457 return 0
2458
2459 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2460 if not branches:
2461 print('No local branch found.')
2462 return 0
2463
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002464 changes = (
2465 Changelist(branchref=b, auth_config=auth_config)
2466 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002467 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002468 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002469 alignment = max(5, max(len(b) for b in branches))
2470 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002471 output = get_cl_statuses(branches,
2472 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002473 max_processes=options.maxjobs,
2474 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002475
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002476 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002477 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002478 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002479 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002480 b, i, status = output.next()
2481 branch_statuses[b] = (i, status)
2482 issue_url, status = branch_statuses.pop(branch)
2483 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002484 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00002485 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00002486 color = ''
2487 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002488 status_str = '(%s)' % status if status else ''
2489 print ' %*s : %s%s %s%s' % (
2490 alignment, ShortBranchName(branch), color, issue_url, status_str,
2491 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002492
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002493 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002494 print
2495 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002496 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002497 if not cl.GetIssue():
2498 print 'No issue assigned.'
2499 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002500 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002501 if not options.fast:
2502 print 'Issue description:'
2503 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002504 return 0
2505
2506
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002507def colorize_CMDstatus_doc():
2508 """To be called once in main() to add colors to git cl status help."""
2509 colors = [i for i in dir(Fore) if i[0].isupper()]
2510
2511 def colorize_line(line):
2512 for color in colors:
2513 if color in line.upper():
2514 # Extract whitespaces first and the leading '-'.
2515 indent = len(line) - len(line.lstrip(' ')) + 1
2516 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2517 return line
2518
2519 lines = CMDstatus.__doc__.splitlines()
2520 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2521
2522
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002523@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002524def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002525 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002526
2527 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002528 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002529 parser.add_option('-r', '--reverse', action='store_true',
2530 help='Lookup the branch(es) for the specified issues. If '
2531 'no issues are specified, all branches with mapped '
2532 'issues will be listed.')
2533 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002534
dnj@chromium.org406c4402015-03-03 17:22:28 +00002535 if options.reverse:
2536 branches = RunGit(['for-each-ref', 'refs/heads',
2537 '--format=%(refname:short)']).splitlines()
2538
2539 # Reverse issue lookup.
2540 issue_branch_map = {}
2541 for branch in branches:
2542 cl = Changelist(branchref=branch)
2543 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2544 if not args:
2545 args = sorted(issue_branch_map.iterkeys())
2546 for issue in args:
2547 if not issue:
2548 continue
2549 print 'Branch for issue number %s: %s' % (
2550 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2551 else:
2552 cl = Changelist()
2553 if len(args) > 0:
2554 try:
2555 issue = int(args[0])
2556 except ValueError:
2557 DieWithError('Pass a number to set the issue or none to list it.\n'
2558 'Maybe you want to run git cl status?')
2559 cl.SetIssue(issue)
2560 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002561 return 0
2562
2563
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002564def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002565 """Shows or posts review comments for any changelist."""
2566 parser.add_option('-a', '--add-comment', dest='comment',
2567 help='comment to add to an issue')
2568 parser.add_option('-i', dest='issue',
2569 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002570 parser.add_option('-j', '--json-file',
2571 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002572 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002573 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002574 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002575
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002576 issue = None
2577 if options.issue:
2578 try:
2579 issue = int(options.issue)
2580 except ValueError:
2581 DieWithError('A review issue id is expected to be a number')
2582
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002583 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002584
2585 if options.comment:
2586 cl.AddComment(options.comment)
2587 return 0
2588
2589 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002590 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002591 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002592 summary.append({
2593 'date': message['date'],
2594 'lgtm': False,
2595 'message': message['text'],
2596 'not_lgtm': False,
2597 'sender': message['sender'],
2598 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002599 if message['disapproval']:
2600 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002601 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002602 elif message['approval']:
2603 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002604 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002605 elif message['sender'] == data['owner_email']:
2606 color = Fore.MAGENTA
2607 else:
2608 color = Fore.BLUE
2609 print '\n%s%s %s%s' % (
2610 color, message['date'].split('.', 1)[0], message['sender'],
2611 Fore.RESET)
2612 if message['text'].strip():
2613 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002614 if options.json_file:
2615 with open(options.json_file, 'wb') as f:
2616 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002617 return 0
2618
2619
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002620def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002621 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002622 parser.add_option('-d', '--display', action='store_true',
2623 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002624 auth.add_auth_options(parser)
2625 options, _ = parser.parse_args(args)
2626 auth_config = auth.extract_auth_config_from_options(options)
2627 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002628 if not cl.GetIssue():
2629 DieWithError('This branch has no associated changelist.')
2630 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002631 if options.display:
2632 print description.description
2633 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002634 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002635 if cl.GetDescription() != description.description:
2636 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002637 return 0
2638
2639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640def CreateDescriptionFromLog(args):
2641 """Pulls out the commit log to use as a base for the CL description."""
2642 log_args = []
2643 if len(args) == 1 and not args[0].endswith('.'):
2644 log_args = [args[0] + '..']
2645 elif len(args) == 1 and args[0].endswith('...'):
2646 log_args = [args[0][:-1]]
2647 elif len(args) == 2:
2648 log_args = [args[0] + '..' + args[1]]
2649 else:
2650 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002651 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002652
2653
thestig@chromium.org44202a22014-03-11 19:22:18 +00002654def CMDlint(parser, args):
2655 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002656 parser.add_option('--filter', action='append', metavar='-x,+y',
2657 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002658 auth.add_auth_options(parser)
2659 options, args = parser.parse_args(args)
2660 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002661
2662 # Access to a protected member _XX of a client class
2663 # pylint: disable=W0212
2664 try:
2665 import cpplint
2666 import cpplint_chromium
2667 except ImportError:
2668 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2669 return 1
2670
2671 # Change the current working directory before calling lint so that it
2672 # shows the correct base.
2673 previous_cwd = os.getcwd()
2674 os.chdir(settings.GetRoot())
2675 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002676 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002677 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2678 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002679 if not files:
2680 print "Cannot lint an empty CL"
2681 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002682
2683 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002684 command = args + files
2685 if options.filter:
2686 command = ['--filter=' + ','.join(options.filter)] + command
2687 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002688
2689 white_regex = re.compile(settings.GetLintRegex())
2690 black_regex = re.compile(settings.GetLintIgnoreRegex())
2691 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2692 for filename in filenames:
2693 if white_regex.match(filename):
2694 if black_regex.match(filename):
2695 print "Ignoring file %s" % filename
2696 else:
2697 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2698 extra_check_functions)
2699 else:
2700 print "Skipping file %s" % filename
2701 finally:
2702 os.chdir(previous_cwd)
2703 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2704 if cpplint._cpplint_state.error_count != 0:
2705 return 1
2706 return 0
2707
2708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002709def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002710 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002711 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002712 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002713 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002714 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002715 auth.add_auth_options(parser)
2716 options, args = parser.parse_args(args)
2717 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002718
sbc@chromium.org71437c02015-04-09 19:29:40 +00002719 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002720 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002721 return 1
2722
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002723 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002724 if args:
2725 base_branch = args[0]
2726 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002727 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002728 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002730 cl.RunHook(
2731 committing=not options.upload,
2732 may_prompt=False,
2733 verbose=options.verbose,
2734 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002735 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002736
2737
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002738def AddChangeIdToCommitMessage(options, args):
2739 """Re-commits using the current message, assumes the commit hook is in
2740 place.
2741 """
2742 log_desc = options.message or CreateDescriptionFromLog(args)
2743 git_command = ['commit', '--amend', '-m', log_desc]
2744 RunGit(git_command)
2745 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002746 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002747 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002748 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002749 else:
2750 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2751
2752
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002753def GenerateGerritChangeId(message):
2754 """Returns Ixxxxxx...xxx change id.
2755
2756 Works the same way as
2757 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2758 but can be called on demand on all platforms.
2759
2760 The basic idea is to generate git hash of a state of the tree, original commit
2761 message, author/committer info and timestamps.
2762 """
2763 lines = []
2764 tree_hash = RunGitSilent(['write-tree'])
2765 lines.append('tree %s' % tree_hash.strip())
2766 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2767 if code == 0:
2768 lines.append('parent %s' % parent.strip())
2769 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2770 lines.append('author %s' % author.strip())
2771 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2772 lines.append('committer %s' % committer.strip())
2773 lines.append('')
2774 # Note: Gerrit's commit-hook actually cleans message of some lines and
2775 # whitespace. This code is not doing this, but it clearly won't decrease
2776 # entropy.
2777 lines.append(message)
2778 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2779 stdin='\n'.join(lines))
2780 return 'I%s' % change_hash.strip()
2781
2782
piman@chromium.org336f9122014-09-04 02:16:55 +00002783def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002784 """upload the current branch to gerrit."""
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002785 # TODO(tandrii): refactor this to be a method of _GerritChangelistImpl,
2786 # to avoid private members accessors below.
2787
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002788 # We assume the remote called "origin" is the one we want.
2789 # It is probably not worthwhile to support different workflows.
2790 gerrit_remote = 'origin'
2791
luqui@chromium.org609f3952015-05-04 22:47:04 +00002792 remote, remote_branch = cl.GetRemoteBranch()
2793 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2794 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002795
andybons@chromium.org962f9462016-02-03 20:00:42 +00002796 if options.title:
2797 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002798 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002799
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002800 if options.squash:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002801 if not cl.GetIssue():
2802 # TODO(tandrii): deperecate this after 2016Q2.
2803 # Backwards compatibility with shadow branch, which used to contain
2804 # change-id for a given branch, using which we can fetch actual issue
2805 # number and set it as the property of the branch, which is the new way.
2806 message = RunGitSilent(['show', '--format=%B', '-s',
2807 'refs/heads/git_cl_uploads/%s' % cl.GetBranch()])
2808 if message:
2809 change_ids = git_footers.get_footer_change_id(message.strip())
2810 if change_ids and len(change_ids) == 1:
2811 details = gerrit_util.GetChangeDetail(
2812 cl._codereview_impl._GetGerritHost(), change_ids[0])
2813 if details:
2814 print('WARNING: found old upload in branch git_cl_uploads/%s '
2815 'corresponding to issue %s' %
2816 (cl.GetBranch(), details['_number']))
2817 cl.SetIssue(details['_number'])
2818 if not cl.GetIssue():
2819 DieWithError(
2820 '\n' # For readability of the blob below.
2821 'Found old upload in branch git_cl_uploads/%s, '
2822 'but failed to find corresponding Gerrit issue.\n'
2823 'If you know the issue number, set it manually first:\n'
2824 ' git cl issue 123456\n'
2825 'If you intended to upload this CL as new issue, '
2826 'just delete or rename the old upload branch:\n'
2827 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2828 'After that, please run git cl upload again.' %
2829 tuple([cl.GetBranch()] * 3))
2830 # End of backwards compatability.
2831
2832 if cl.GetIssue():
2833 # Try to get the message from a previous upload.
2834 message = cl.GetDescription()
2835 if not message:
2836 DieWithError(
2837 'failed to fetch description from current Gerrit issue %d\n'
2838 '%s' % (cl.GetIssue(), cl.GetIssueURL()))
2839 change_id = cl._codereview_impl._GetChangeDetail([])['change_id']
2840 while True:
2841 footer_change_ids = git_footers.get_footer_change_id(message)
2842 if footer_change_ids == [change_id]:
2843 break
2844 if not footer_change_ids:
2845 message = git_footers.add_footer_change_id(message, change_id)
2846 print('WARNING: appended missing Change-Id to issue description')
2847 continue
2848 # There is already a valid footer but with different or several ids.
2849 # Doing this automatically is non-trivial as we don't want to lose
2850 # existing other footers, yet we want to append just 1 desired
2851 # Change-Id. Thus, just create a new footer, but let user verify the new
2852 # description.
2853 message = '%s\n\nChange-Id: %s' % (message, change_id)
2854 print(
2855 'WARNING: issue %s has Change-Id footer(s):\n'
2856 ' %s\n'
2857 'but issue has Change-Id %s, according to Gerrit.\n'
2858 'Please, check the proposed correction to the description, '
2859 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2860 % (cl.GetIssue(), '\n '.join(footer_change_ids), change_id,
2861 change_id))
2862 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2863 if not options.force:
2864 change_desc = ChangeDescription(message)
2865 change_desc.prompt()
2866 message = change_desc.description
2867 if not message:
2868 DieWithError("Description is empty. Aborting...")
2869 # Continue the while loop.
2870 # Sanity check of this code - we should end up with proper message footer.
2871 assert [change_id] == git_footers.get_footer_change_id(message)
2872 change_desc = ChangeDescription(message)
2873 else:
2874 change_desc = ChangeDescription(
2875 options.message or CreateDescriptionFromLog(args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002876 if not options.force:
2877 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002878 if not change_desc.description:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002879 DieWithError("Description is empty. Aborting...")
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002880 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002881 change_ids = git_footers.get_footer_change_id(message)
2882 if len(change_ids) > 1:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002883 DieWithError('too many Change-Id footers, at most 1 allowed.')
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002884 if not change_ids:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002885 # Generate the Change-Id automatically.
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002886 message = git_footers.add_footer_change_id(
2887 message, GenerateGerritChangeId(message))
2888 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002889 change_ids = git_footers.get_footer_change_id(message)
2890 assert len(change_ids) == 1
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002891 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002892
2893 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2894 if remote is '.':
2895 # If our upstream branch is local, we base our squashed commit on its
2896 # squashed version.
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002897 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2898 # Check the squashed hash of the parent.
2899 parent = RunGit(['config',
2900 'branch.%s.gerritsquashhash' % upstream_branch_name],
2901 error_ok=True).strip()
2902 # Verify that the upstream branch has been uploaded too, otherwise
2903 # Gerrit will create additional CLs when uploading.
2904 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2905 RunGitSilent(['rev-parse', parent + ':'])):
2906 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2907 DieWithError(
2908 'Upload upstream branch %s first.\n'
2909 'Note: maybe you\'ve uploaded it with --no-squash or with an old\n'
2910 ' version of depot_tools. If so, then re-upload it with:\n'
2911 ' git cl upload --squash\n' % upstream_branch_name)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002912 else:
2913 parent = cl.GetCommonAncestorWithUpstream()
2914
2915 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2916 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2917 '-m', message]).strip()
2918 else:
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002919 change_desc = ChangeDescription(
2920 options.message or CreateDescriptionFromLog(args))
2921 if not change_desc.description:
2922 DieWithError("Description is empty. Aborting...")
2923
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002924 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002925 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002926 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002927 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002928 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002929 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002930
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002931 assert change_desc
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002932 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2933 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002934 if len(commits) > 1:
2935 print('WARNING: This will upload %d commits. Run the following command '
2936 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002937 print('git log %s..%s' % (parent, ref_to_push))
2938 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002939 'commit.')
2940 ask_for_data('About to upload; enter to confirm.')
2941
piman@chromium.org336f9122014-09-04 02:16:55 +00002942 if options.reviewers or options.tbr_owners:
2943 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002944
ukai@chromium.orge8077812012-02-03 03:41:46 +00002945 receive_options = []
2946 cc = cl.GetCCList().split(',')
2947 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002948 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002949 cc = filter(None, cc)
2950 if cc:
2951 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002952 if change_desc.get_reviewers():
2953 receive_options.extend(
2954 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002955
ukai@chromium.orge8077812012-02-03 03:41:46 +00002956 git_command = ['push']
2957 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002958 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002959 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002960 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002961 push_stdout = gclient_utils.CheckCallAndFilter(
2962 ['git'] + git_command,
2963 print_stdout=True,
2964 # Flush after every line: useful for seeing progress when running as
2965 # recipe.
2966 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002967
2968 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002969 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2970 change_numbers = [m.group(1)
2971 for m in map(regex.match, push_stdout.splitlines())
2972 if m]
2973 if len(change_numbers) != 1:
2974 DieWithError(
2975 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2976 'Change-Id: %s') % (len(change_numbers), change_id))
2977 cl.SetIssue(change_numbers[0])
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00002978 RunGit(['config', 'branch.%s.gerritsquashhash' % cl.GetBranch(),
2979 ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002980 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002981
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002982
wittman@chromium.org455dc922015-01-26 20:15:50 +00002983def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2984 """Computes the remote branch ref to use for the CL.
2985
2986 Args:
2987 remote (str): The git remote for the CL.
2988 remote_branch (str): The git remote branch for the CL.
2989 target_branch (str): The target branch specified by the user.
2990 pending_prefix (str): The pending prefix from the settings.
2991 """
2992 if not (remote and remote_branch):
2993 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002994
wittman@chromium.org455dc922015-01-26 20:15:50 +00002995 if target_branch:
2996 # Cannonicalize branch references to the equivalent local full symbolic
2997 # refs, which are then translated into the remote full symbolic refs
2998 # below.
2999 if '/' not in target_branch:
3000 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3001 else:
3002 prefix_replacements = (
3003 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3004 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3005 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3006 )
3007 match = None
3008 for regex, replacement in prefix_replacements:
3009 match = re.search(regex, target_branch)
3010 if match:
3011 remote_branch = target_branch.replace(match.group(0), replacement)
3012 break
3013 if not match:
3014 # This is a branch path but not one we recognize; use as-is.
3015 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003016 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3017 # Handle the refs that need to land in different refs.
3018 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003019
wittman@chromium.org455dc922015-01-26 20:15:50 +00003020 # Create the true path to the remote branch.
3021 # Does the following translation:
3022 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3023 # * refs/remotes/origin/master -> refs/heads/master
3024 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3025 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3026 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3027 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3028 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3029 'refs/heads/')
3030 elif remote_branch.startswith('refs/remotes/branch-heads'):
3031 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3032 # If a pending prefix exists then replace refs/ with it.
3033 if pending_prefix:
3034 remote_branch = remote_branch.replace('refs/', pending_prefix)
3035 return remote_branch
3036
3037
piman@chromium.org336f9122014-09-04 02:16:55 +00003038def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003039 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003040 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003041 upload_args.extend(['--server', cl.GetCodereviewServer()])
3042 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
3043 upload_args.extend(auth.auth_config_to_command_options(
3044 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003045 if options.emulate_svn_auto_props:
3046 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003047
3048 change_desc = None
3049
pgervais@chromium.org91141372014-01-09 23:27:20 +00003050 if options.email is not None:
3051 upload_args.extend(['--email', options.email])
3052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003053 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003054 if options.title:
3055 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00003056 if options.message:
3057 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00003058 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059 print ("This branch is associated with issue %s. "
3060 "Adding patch to that issue." % cl.GetIssue())
3061 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003062 if options.title:
3063 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00003064 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003065 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00003066 if options.reviewers or options.tbr_owners:
3067 change_desc.update_reviewers(options.reviewers,
3068 options.tbr_owners,
3069 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003070 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003071 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003072
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003073 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003074 print "Description is empty; aborting."
3075 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003076
maruel@chromium.org71e12a92012-02-14 02:34:15 +00003077 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003078 if change_desc.get_reviewers():
3079 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00003080 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003081 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00003082 DieWithError("Must specify reviewers to send email.")
3083 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00003084
3085 # We check this before applying rietveld.private assuming that in
3086 # rietveld.cc only addresses which we can send private CLs to are listed
3087 # if rietveld.private is set, and so we should ignore rietveld.cc only when
3088 # --private is specified explicitly on the command line.
3089 if options.private:
3090 logging.warn('rietveld.cc is ignored since private flag is specified. '
3091 'You need to review and add them manually if necessary.')
3092 cc = cl.GetCCListWithoutDefault()
3093 else:
3094 cc = cl.GetCCList()
3095 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00003096 if cc:
3097 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003099 if options.private or settings.GetDefaultPrivateFlag() == "True":
3100 upload_args.append('--private')
3101
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003102 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00003103 if not options.find_copies:
3104 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003105
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003106 # Include the upstream repo's URL in the change -- this is useful for
3107 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003108 remote_url = cl.GetGitBaseUrlFromConfig()
3109 if not remote_url:
3110 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003111 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003112 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00003113 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
3114 remote_url = (cl.GetRemoteUrl() + '@'
3115 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003116 if remote_url:
3117 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00003118 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00003119 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
3120 settings.GetPendingRefPrefix())
3121 if target_ref:
3122 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003123
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003124 # Look for dependent patchsets. See crbug.com/480453 for more details.
3125 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3126 upstream_branch = ShortBranchName(upstream_branch)
3127 if remote is '.':
3128 # A local branch is being tracked.
3129 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00003130 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003131 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003132 print ('Skipping dependency patchset upload because git config '
3133 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003134 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00003135 else:
3136 auth_config = auth.extract_auth_config_from_options(options)
3137 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
3138 branch_cl_issue_url = branch_cl.GetIssueURL()
3139 branch_cl_issue = branch_cl.GetIssue()
3140 branch_cl_patchset = branch_cl.GetPatchset()
3141 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
3142 upload_args.extend(
3143 ['--depends_on_patchset', '%s:%s' % (
3144 branch_cl_issue, branch_cl_patchset)])
3145 print
3146 print ('The current branch (%s) is tracking a local branch (%s) with '
3147 'an associated CL.') % (cl.GetBranch(), local_branch)
3148 print 'Adding %s/#ps%s as a dependency patchset.' % (
3149 branch_cl_issue_url, branch_cl_patchset)
3150 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00003151
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003152 project = settings.GetProject()
3153 if project:
3154 upload_args.extend(['--project', project])
3155
rmistry@google.comef966222015-04-07 11:15:01 +00003156 if options.cq_dry_run:
3157 upload_args.extend(['--cq_dry_run'])
3158
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003159 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00003160 upload_args = ['upload'] + upload_args + args
3161 logging.info('upload.RealMain(%s)', upload_args)
3162 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00003163 issue = int(issue)
3164 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00003165 except KeyboardInterrupt:
3166 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003167 except:
3168 # If we got an exception after the user typed a description for their
3169 # change, back up the description before re-raising.
3170 if change_desc:
3171 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
3172 print '\nGot exception while uploading -- saving description to %s\n' \
3173 % backup_path
3174 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003175 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003176 backup_file.close()
3177 raise
3178
3179 if not cl.GetIssue():
3180 cl.SetIssue(issue)
3181 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003182
3183 if options.use_commit_queue:
3184 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003185 return 0
3186
3187
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003188def cleanup_list(l):
3189 """Fixes a list so that comma separated items are put as individual items.
3190
3191 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3192 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3193 """
3194 items = sum((i.split(',') for i in l), [])
3195 stripped_items = (i.strip() for i in items)
3196 return sorted(filter(None, stripped_items))
3197
3198
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003199@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003200def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003201 """Uploads the current changelist to codereview.
3202
3203 Can skip dependency patchset uploads for a branch by running:
3204 git config branch.branch_name.skip-deps-uploads True
3205 To unset run:
3206 git config --unset branch.branch_name.skip-deps-uploads
3207 Can also set the above globally by using the --global flag.
3208 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003209 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3210 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003211 parser.add_option('--bypass-watchlists', action='store_true',
3212 dest='bypass_watchlists',
3213 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003214 parser.add_option('-f', action='store_true', dest='force',
3215 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003216 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003217 parser.add_option('-t', dest='title',
3218 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003219 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003220 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003221 help='reviewer email addresses')
3222 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003223 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003224 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003225 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003226 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003227 parser.add_option('--emulate_svn_auto_props',
3228 '--emulate-svn-auto-props',
3229 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003230 dest="emulate_svn_auto_props",
3231 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003232 parser.add_option('-c', '--use-commit-queue', action='store_true',
3233 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003234 parser.add_option('--private', action='store_true',
3235 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003236 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003237 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003238 metavar='TARGET',
3239 help='Apply CL to remote ref TARGET. ' +
3240 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003241 parser.add_option('--squash', action='store_true',
3242 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003243 parser.add_option('--no-squash', action='store_true',
3244 help='Don\'t squash multiple commits into one ' +
3245 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003246 parser.add_option('--email', default=None,
3247 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003248 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3249 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003250 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3251 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003252 help='Send the patchset to do a CQ dry run right after '
3253 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003254 parser.add_option('--dependencies', action='store_true',
3255 help='Uploads CLs of all the local branches that depend on '
3256 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003257
rmistry@google.com2dd99862015-06-22 12:22:18 +00003258 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003259 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003260 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003261 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003262 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003263
sbc@chromium.org71437c02015-04-09 19:29:40 +00003264 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003265 return 1
3266
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003267 options.reviewers = cleanup_list(options.reviewers)
3268 options.cc = cleanup_list(options.cc)
3269
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003270 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3271 settings.GetIsGerrit()
3272
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003273 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003274 if args:
3275 # TODO(ukai): is it ok for gerrit case?
3276 base_branch = args[0]
3277 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00003278 if cl.GetBranch() is None:
3279 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
3280
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003281 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003282 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00003283 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00003284
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003285 # Make sure authenticated to Rietveld before running expensive hooks. It is
3286 # a fast, best efforts check. Rietveld still can reject the authentication
3287 # during the actual upload.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003288 if not cl.IsGerrit() and auth_config.use_oauth2:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003289 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003290 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003291 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003292 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003293
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003294 # Apply watchlists on upload.
3295 change = cl.GetChange(base_branch, None)
3296 watchlist = watchlists.Watchlists(change.RepositoryRoot())
3297 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003298 if not options.bypass_watchlists:
3299 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003300
ukai@chromium.orge8077812012-02-03 03:41:46 +00003301 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00003302 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003303 # Set the reviewer list now so that presubmit checks can access it.
3304 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00003305 change_description.update_reviewers(options.reviewers,
3306 options.tbr_owners,
3307 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00003308 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003309 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00003310 may_prompt=not options.force,
3311 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003312 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003313 if not hook_results.should_continue():
3314 return 1
3315 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003316 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003317
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003318 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003319 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003320 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00003321 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003322 print ('The last upload made from this repository was patchset #%d but '
3323 'the most recent patchset on the server is #%d.'
3324 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00003325 print ('Uploading will still work, but if you\'ve uploaded to this issue '
3326 'from another machine or branch the patch you\'re uploading now '
3327 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00003328 ask_for_data('About to upload; enter to confirm.')
3329
iannucci@chromium.org79540052012-10-19 23:15:26 +00003330 print_stats(options.similarity, options.find_copies, args)
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003331 if cl.IsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003332 if options.squash and options.no_squash:
3333 DieWithError('Can only use one of --squash or --no-squash')
3334
3335 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
3336 not options.no_squash)
3337
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00003338 ret = GerritUpload(options, args, cl, change)
3339 else:
3340 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003341 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00003342 git_set_branch_value('last-upload-hash',
3343 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00003344 # Run post upload hooks, if specified.
3345 if settings.GetRunPostUploadHook():
3346 presubmit_support.DoPostUploadExecuter(
3347 change,
3348 cl,
3349 settings.GetRoot(),
3350 options.verbose,
3351 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003352
rmistry@google.com2dd99862015-06-22 12:22:18 +00003353 # Upload all dependencies if specified.
3354 if options.dependencies:
3355 print
3356 print '--dependencies has been specified.'
3357 print 'All dependent local branches will be re-uploaded.'
3358 print
3359 # Remove the dependencies flag from args so that we do not end up in a
3360 # loop.
3361 orig_args.remove('--dependencies')
3362 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00003363 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00003364
3365
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003366def IsSubmoduleMergeCommit(ref):
3367 # When submodules are added to the repo, we expect there to be a single
3368 # non-git-svn merge commit at remote HEAD with a signature comment.
3369 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003370 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003371 return RunGit(cmd) != ''
3372
3373
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003374def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003375 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003376
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003377 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3378 upstream and closes the issue automatically and atomically.
3379
3380 Otherwise (in case of Rietveld):
3381 Squashes branch into a single commit.
3382 Updates changelog with metadata (e.g. pointer to review).
3383 Pushes/dcommits the code upstream.
3384 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003385 """
3386 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3387 help='bypass upload presubmit hook')
3388 parser.add_option('-m', dest='message',
3389 help="override review description")
3390 parser.add_option('-f', action='store_true', dest='force',
3391 help="force yes to questions (don't prompt)")
3392 parser.add_option('-c', dest='contributor',
3393 help="external contributor for patch (appended to " +
3394 "description and used as author for git). Should be " +
3395 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003396 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003397 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003399 auth_config = auth.extract_auth_config_from_options(options)
3400
3401 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003402
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003403 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3404 if cl.IsGerrit():
3405 if options.message:
3406 # This could be implemented, but it requires sending a new patch to
3407 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3408 # Besides, Gerrit has the ability to change the commit message on submit
3409 # automatically, thus there is no need to support this option (so far?).
3410 parser.error('-m MESSAGE option is not supported for Gerrit.')
3411 if options.contributor:
3412 parser.error(
3413 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3414 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3415 'the contributor\'s "name <email>". If you can\'t upload such a '
3416 'commit for review, contact your repository admin and request'
3417 '"Forge-Author" permission.')
3418 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3419 options.verbose)
3420
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003421 current = cl.GetBranch()
3422 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3423 if not settings.GetIsGitSvn() and remote == '.':
3424 print
3425 print 'Attempting to push branch %r into another local branch!' % current
3426 print
3427 print 'Either reparent this branch on top of origin/master:'
3428 print ' git reparent-branch --root'
3429 print
3430 print 'OR run `git rebase-update` if you think the parent branch is already'
3431 print 'committed.'
3432 print
3433 print ' Current parent: %r' % upstream_branch
3434 return 1
3435
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003436 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003437 # Default to merging against our best guess of the upstream branch.
3438 args = [cl.GetUpstreamBranch()]
3439
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003440 if options.contributor:
3441 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3442 print "Please provide contibutor as 'First Last <email@example.com>'"
3443 return 1
3444
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003445 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003446 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003447
sbc@chromium.org71437c02015-04-09 19:29:40 +00003448 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003449 return 1
3450
3451 # This rev-list syntax means "show all commits not in my branch that
3452 # are in base_branch".
3453 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3454 base_branch]).splitlines()
3455 if upstream_commits:
3456 print ('Base branch "%s" has %d commits '
3457 'not in this branch.' % (base_branch, len(upstream_commits)))
3458 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3459 return 1
3460
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003461 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003462 svn_head = None
3463 if cmd == 'dcommit' or base_has_submodules:
3464 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3465 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003466
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003467 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003468 # If the base_head is a submodule merge commit, the first parent of the
3469 # base_head should be a git-svn commit, which is what we're interested in.
3470 base_svn_head = base_branch
3471 if base_has_submodules:
3472 base_svn_head += '^1'
3473
3474 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003475 if extra_commits:
3476 print ('This branch has %d additional commits not upstreamed yet.'
3477 % len(extra_commits.splitlines()))
3478 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3479 'before attempting to %s.' % (base_branch, cmd))
3480 return 1
3481
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003482 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003483 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003484 author = None
3485 if options.contributor:
3486 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003487 hook_results = cl.RunHook(
3488 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003489 may_prompt=not options.force,
3490 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003491 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003492 if not hook_results.should_continue():
3493 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003494
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003495 # Check the tree status if the tree status URL is set.
3496 status = GetTreeStatus()
3497 if 'closed' == status:
3498 print('The tree is closed. Please wait for it to reopen. Use '
3499 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3500 return 1
3501 elif 'unknown' == status:
3502 print('Unable to determine tree status. Please verify manually and '
3503 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3504 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003505
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003506 change_desc = ChangeDescription(options.message)
3507 if not change_desc.description and cl.GetIssue():
3508 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003509
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003510 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003511 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003512 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003513 else:
3514 print 'No description set.'
3515 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3516 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003517
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003518 # Keep a separate copy for the commit message, because the commit message
3519 # contains the link to the Rietveld issue, while the Rietveld message contains
3520 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003521 # Keep a separate copy for the commit message.
3522 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003523 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003524
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003525 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003526 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003527 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003528 # after it. Add a period on a new line to circumvent this. Also add a space
3529 # before the period to make sure that Gitiles continues to correctly resolve
3530 # the URL.
3531 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003533 commit_desc.append_footer('Patch from %s.' % options.contributor)
3534
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003535 print('Description:')
3536 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003538 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003539 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003540 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003541
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003542 # We want to squash all this branch's commits into one commit with the proper
3543 # description. We do this by doing a "reset --soft" to the base branch (which
3544 # keeps the working copy the same), then dcommitting that. If origin/master
3545 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3546 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003548 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3549 # Delete the branches if they exist.
3550 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3551 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3552 result = RunGitWithCode(showref_cmd)
3553 if result[0] == 0:
3554 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003555
3556 # We might be in a directory that's present in this branch but not in the
3557 # trunk. Move up to the top of the tree so that git commands that expect a
3558 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003559 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003560 if rel_base_path:
3561 os.chdir(rel_base_path)
3562
3563 # Stuff our change into the merge branch.
3564 # We wrap in a try...finally block so if anything goes wrong,
3565 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003566 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003567 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003568 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003569 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003570 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003571 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003572 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003573 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003574 RunGit(
3575 [
3576 'commit', '--author', options.contributor,
3577 '-m', commit_desc.description,
3578 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003579 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003580 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003581 if base_has_submodules:
3582 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3583 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3584 RunGit(['checkout', CHERRY_PICK_BRANCH])
3585 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003586 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003587 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003588 mirror = settings.GetGitMirror(remote)
3589 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003590 pending_prefix = settings.GetPendingRefPrefix()
3591 if not pending_prefix or branch.startswith(pending_prefix):
3592 # If not using refs/pending/heads/* at all, or target ref is already set
3593 # to pending, then push to the target ref directly.
3594 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003595 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003596 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003597 else:
3598 # Cherry-pick the change on top of pending ref and then push it.
3599 assert branch.startswith('refs/'), branch
3600 assert pending_prefix[-1] == '/', pending_prefix
3601 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003602 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003603 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003604 if retcode == 0:
3605 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003606 else:
3607 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003608 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003609 'svn', 'dcommit',
3610 '-C%s' % options.similarity,
3611 '--no-rebase', '--rmdir',
3612 ]
3613 if settings.GetForceHttpsCommitUrl():
3614 # Allow forcing https commit URLs for some projects that don't allow
3615 # committing to http URLs (like Google Code).
3616 remote_url = cl.GetGitSvnRemoteUrl()
3617 if urlparse.urlparse(remote_url).scheme == 'http':
3618 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003619 cmd_args.append('--commit-url=%s' % remote_url)
3620 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003621 if 'Committed r' in output:
3622 revision = re.match(
3623 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3624 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003625 finally:
3626 # And then swap back to the original branch and clean up.
3627 RunGit(['checkout', '-q', cl.GetBranch()])
3628 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003629 if base_has_submodules:
3630 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003631
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003632 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003633 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003634 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003635
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003636 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003637 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003638 try:
3639 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3640 # We set pushed_to_pending to False, since it made it all the way to the
3641 # real ref.
3642 pushed_to_pending = False
3643 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003644 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003646 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003647 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003648 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003649 if not to_pending:
3650 if viewvc_url and revision:
3651 change_desc.append_footer(
3652 'Committed: %s%s' % (viewvc_url, revision))
3653 elif revision:
3654 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003655 print ('Closing issue '
3656 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003657 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003658 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003659 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003660 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003661 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003662 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003663 if options.bypass_hooks:
3664 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3665 else:
3666 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003667 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003668 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003669
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003670 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003671 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3672 print 'The commit is in the pending queue (%s).' % pending_ref
3673 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003674 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003675 'footer.' % branch)
3676
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003677 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3678 if os.path.isfile(hook):
3679 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003680
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003681 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003682
3683
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003684def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3685 print
3686 print 'Waiting for commit to be landed on %s...' % real_ref
3687 print '(If you are impatient, you may Ctrl-C once without harm)'
3688 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3689 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003690 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003691
3692 loop = 0
3693 while True:
3694 sys.stdout.write('fetching (%d)... \r' % loop)
3695 sys.stdout.flush()
3696 loop += 1
3697
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003698 if mirror:
3699 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003700 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3701 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3702 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3703 for commit in commits.splitlines():
3704 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3705 print 'Found commit on %s' % real_ref
3706 return commit
3707
3708 current_rev = to_rev
3709
3710
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003711def PushToGitPending(remote, pending_ref, upstream_ref):
3712 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3713
3714 Returns:
3715 (retcode of last operation, output log of last operation).
3716 """
3717 assert pending_ref.startswith('refs/'), pending_ref
3718 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3719 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3720 code = 0
3721 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003722 max_attempts = 3
3723 attempts_left = max_attempts
3724 while attempts_left:
3725 if attempts_left != max_attempts:
3726 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3727 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003728
3729 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003730 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003731 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003732 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003733 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003734 print 'Fetch failed with exit code %d.' % code
3735 if out.strip():
3736 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003737 continue
3738
3739 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003740 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003741 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003742 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003743 if code:
3744 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003745 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3746 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003747 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3748 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003749 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003750 return code, out
3751
3752 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003753 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003754 code, out = RunGitWithCode(
3755 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3756 if code == 0:
3757 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003758 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003759 return code, out
3760
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003761 print 'Push failed with exit code %d.' % code
3762 if out.strip():
3763 print out.strip()
3764 if IsFatalPushFailure(out):
3765 print (
3766 'Fatal push error. Make sure your .netrc credentials and git '
3767 'user.email are correct and you have push access to the repo.')
3768 return code, out
3769
3770 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003771 return code, out
3772
3773
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003774def IsFatalPushFailure(push_stdout):
3775 """True if retrying push won't help."""
3776 return '(prohibited by Gerrit)' in push_stdout
3777
3778
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003779@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003780def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003781 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003782 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003783 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003784 # If it looks like previous commits were mirrored with git-svn.
3785 message = """This repository appears to be a git-svn mirror, but no
3786upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3787 else:
3788 message = """This doesn't appear to be an SVN repository.
3789If your project has a true, writeable git repository, you probably want to run
3790'git cl land' instead.
3791If your project has a git mirror of an upstream SVN master, you probably need
3792to run 'git svn init'.
3793
3794Using the wrong command might cause your commit to appear to succeed, and the
3795review to be closed, without actually landing upstream. If you choose to
3796proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003797 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003798 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003799 return SendUpstream(parser, args, 'dcommit')
3800
3801
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003802@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003803def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003804 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003805 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003806 print('This appears to be an SVN repository.')
3807 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003808 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003809 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003810 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811
3812
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003813@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003814def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003815 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003816 parser.add_option('-b', dest='newbranch',
3817 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003818 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003819 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003820 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3821 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003822 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003823 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003824 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003825 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003826 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003827 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003828
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003829
3830 group = optparse.OptionGroup(
3831 parser,
3832 'Options for continuing work on the current issue uploaded from a '
3833 'different clone (e.g. different machine). Must be used independently '
3834 'from the other options. No issue number should be specified, and the '
3835 'branch must have an issue number associated with it')
3836 group.add_option('--reapply', action='store_true', dest='reapply',
3837 help='Reset the branch and reapply the issue.\n'
3838 'CAUTION: This will undo any local changes in this '
3839 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003840
3841 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003842 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003843 parser.add_option_group(group)
3844
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003845 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003846 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003847 auth_config = auth.extract_auth_config_from_options(options)
3848
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003849 cl = Changelist(auth_config=auth_config)
3850
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003851 issue_arg = None
3852 if options.reapply :
3853 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003854 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003855
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003856 issue_arg = cl.GetIssue()
3857 upstream = cl.GetUpstreamBranch()
3858 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003859 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003860
3861 RunGit(['reset', '--hard', upstream])
3862 if options.pull:
3863 RunGit(['pull'])
3864 else:
3865 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003866 parser.error('Must specify issue number or url')
3867 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003868
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003869 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003870 parser.print_help()
3871 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003872
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003873 if cl.IsGerrit():
3874 if options.reject:
3875 parser.error('--reject is not supported with Gerrit codereview.')
3876 if options.nocommit:
3877 parser.error('--nocommit is not supported with Gerrit codereview.')
3878 if options.directory:
3879 parser.error('--directory is not supported with Gerrit codereview.')
3880
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003881 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003882 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003883 return 1
3884
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003885 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003886 if options.reapply:
3887 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003888 if options.force:
3889 RunGit(['branch', '-D', options.newbranch],
3890 stderr=subprocess2.PIPE, error_ok=True)
3891 RunGit(['checkout', '-b', options.newbranch,
3892 Changelist().GetUpstreamBranch()])
3893
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003894 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3895 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003896
3897
3898def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003899 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003900 # Provide a wrapper for git svn rebase to help avoid accidental
3901 # git svn dcommit.
3902 # It's the only command that doesn't use parser at all since we just defer
3903 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003904
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003905 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003906
3907
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003908def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003909 """Fetches the tree status and returns either 'open', 'closed',
3910 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003911 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912 if url:
3913 status = urllib2.urlopen(url).read().lower()
3914 if status.find('closed') != -1 or status == '0':
3915 return 'closed'
3916 elif status.find('open') != -1 or status == '1':
3917 return 'open'
3918 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003919 return 'unset'
3920
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003922def GetTreeStatusReason():
3923 """Fetches the tree status from a json url and returns the message
3924 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003925 url = settings.GetTreeStatusUrl()
3926 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003927 connection = urllib2.urlopen(json_url)
3928 status = json.loads(connection.read())
3929 connection.close()
3930 return status['message']
3931
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003932
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003933def GetBuilderMaster(bot_list):
3934 """For a given builder, fetch the master from AE if available."""
3935 map_url = 'https://builders-map.appspot.com/'
3936 try:
3937 master_map = json.load(urllib2.urlopen(map_url))
3938 except urllib2.URLError as e:
3939 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3940 (map_url, e))
3941 except ValueError as e:
3942 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3943 if not master_map:
3944 return None, 'Failed to build master map.'
3945
3946 result_master = ''
3947 for bot in bot_list:
3948 builder = bot.split(':', 1)[0]
3949 master_list = master_map.get(builder, [])
3950 if not master_list:
3951 return None, ('No matching master for builder %s.' % builder)
3952 elif len(master_list) > 1:
3953 return None, ('The builder name %s exists in multiple masters %s.' %
3954 (builder, master_list))
3955 else:
3956 cur_master = master_list[0]
3957 if not result_master:
3958 result_master = cur_master
3959 elif result_master != cur_master:
3960 return None, 'The builders do not belong to the same master.'
3961 return result_master, None
3962
3963
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003964def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003965 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003966 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003967 status = GetTreeStatus()
3968 if 'unset' == status:
3969 print 'You must configure your tree status URL by running "git cl config".'
3970 return 2
3971
3972 print "The tree is %s" % status
3973 print
3974 print GetTreeStatusReason()
3975 if status != 'open':
3976 return 1
3977 return 0
3978
3979
maruel@chromium.org15192402012-09-06 12:38:29 +00003980def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003981 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003982 group = optparse.OptionGroup(parser, "Try job options")
3983 group.add_option(
3984 "-b", "--bot", action="append",
3985 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3986 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003987 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003988 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003989 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003990 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003991 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003992 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003993 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003994 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003995 "-r", "--revision",
3996 help="Revision to use for the try job; default: the "
3997 "revision will be determined by the try server; see "
3998 "its waterfall for more info")
3999 group.add_option(
4000 "-c", "--clobber", action="store_true", default=False,
4001 help="Force a clobber before building; e.g. don't do an "
4002 "incremental build")
4003 group.add_option(
4004 "--project",
4005 help="Override which project to use. Projects are defined "
4006 "server-side to define what default bot set to use")
4007 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004008 "-p", "--property", dest="properties", action="append", default=[],
4009 help="Specify generic properties in the form -p key1=value1 -p "
4010 "key2=value2 etc (buildbucket only). The value will be treated as "
4011 "json if decodable, or as string otherwise.")
4012 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004013 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004014 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004015 "--use-rietveld", action="store_true", default=False,
4016 help="Use Rietveld to trigger try jobs.")
4017 group.add_option(
4018 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4019 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004020 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004021 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004022 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004023 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004024
machenbach@chromium.org45453142015-09-15 08:45:22 +00004025 if options.use_rietveld and options.properties:
4026 parser.error('Properties can only be specified with buildbucket')
4027
4028 # Make sure that all properties are prop=value pairs.
4029 bad_params = [x for x in options.properties if '=' not in x]
4030 if bad_params:
4031 parser.error('Got properties with missing "=": %s' % bad_params)
4032
maruel@chromium.org15192402012-09-06 12:38:29 +00004033 if args:
4034 parser.error('Unknown arguments: %s' % args)
4035
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004036 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004037 if not cl.GetIssue():
4038 parser.error('Need to upload first')
4039
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004040 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004041 if props.get('closed'):
4042 parser.error('Cannot send tryjobs for a closed CL')
4043
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004044 if props.get('private'):
4045 parser.error('Cannot use trybots with private issue')
4046
maruel@chromium.org15192402012-09-06 12:38:29 +00004047 if not options.name:
4048 options.name = cl.GetBranch()
4049
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004050 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004051 options.master, err_msg = GetBuilderMaster(options.bot)
4052 if err_msg:
4053 parser.error('Tryserver master cannot be found because: %s\n'
4054 'Please manually specify the tryserver master'
4055 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004056
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004057 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004058 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004059 if not options.bot:
4060 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004061
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004062 # Get try masters from PRESUBMIT.py files.
4063 masters = presubmit_support.DoGetTryMasters(
4064 change,
4065 change.LocalPaths(),
4066 settings.GetRoot(),
4067 None,
4068 None,
4069 options.verbose,
4070 sys.stdout)
4071 if masters:
4072 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004073
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004074 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4075 options.bot = presubmit_support.DoGetTrySlaves(
4076 change,
4077 change.LocalPaths(),
4078 settings.GetRoot(),
4079 None,
4080 None,
4081 options.verbose,
4082 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004083
4084 if not options.bot:
4085 # Get try masters from cq.cfg if any.
4086 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4087 # location.
4088 cq_cfg = os.path.join(change.RepositoryRoot(),
4089 'infra', 'config', 'cq.cfg')
4090 if os.path.exists(cq_cfg):
4091 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004092 cq_masters = commit_queue.get_master_builder_map(
4093 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004094 for master, builders in cq_masters.iteritems():
4095 for builder in builders:
4096 # Skip presubmit builders, because these will fail without LGTM.
4097 if 'presubmit' not in builder.lower():
4098 masters.setdefault(master, {})[builder] = ['defaulttests']
4099 if masters:
4100 return masters
4101
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004102 if not options.bot:
4103 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004104
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004105 builders_and_tests = {}
4106 # TODO(machenbach): The old style command-line options don't support
4107 # multiple try masters yet.
4108 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4109 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4110
4111 for bot in old_style:
4112 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004113 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004114 elif ',' in bot:
4115 parser.error('Specify one bot per --bot flag')
4116 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004117 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004118
4119 for bot, tests in new_style:
4120 builders_and_tests.setdefault(bot, []).extend(tests)
4121
4122 # Return a master map with one master to be backwards compatible. The
4123 # master name defaults to an empty string, which will cause the master
4124 # not to be set on rietveld (deprecated).
4125 return {options.master: builders_and_tests}
4126
4127 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004128
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004129 for builders in masters.itervalues():
4130 if any('triggered' in b for b in builders):
4131 print >> sys.stderr, (
4132 'ERROR You are trying to send a job to a triggered bot. This type of'
4133 ' bot requires an\ninitial job from a parent (usually a builder). '
4134 'Instead send your job to the parent.\n'
4135 'Bot list: %s' % builders)
4136 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004137
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004138 patchset = cl.GetMostRecentPatchset()
4139 if patchset and patchset != cl.GetPatchset():
4140 print(
4141 '\nWARNING Mismatch between local config and server. Did a previous '
4142 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4143 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004144 if options.luci:
4145 trigger_luci_job(cl, masters, options)
4146 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004147 try:
4148 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4149 except BuildbucketResponseException as ex:
4150 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004151 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004152 except Exception as e:
4153 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4154 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4155 e, stacktrace)
4156 return 1
4157 else:
4158 try:
4159 cl.RpcServer().trigger_distributed_try_jobs(
4160 cl.GetIssue(), patchset, options.name, options.clobber,
4161 options.revision, masters)
4162 except urllib2.HTTPError as e:
4163 if e.code == 404:
4164 print('404 from rietveld; '
4165 'did you mean to use "git try" instead of "git cl try"?')
4166 return 1
4167 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004168
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004169 for (master, builders) in sorted(masters.iteritems()):
4170 if master:
4171 print 'Master: %s' % master
4172 length = max(len(builder) for builder in builders)
4173 for builder in sorted(builders):
4174 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004175 return 0
4176
4177
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004178def CMDtry_results(parser, args):
4179 group = optparse.OptionGroup(parser, "Try job results options")
4180 group.add_option(
4181 "-p", "--patchset", type=int, help="patchset number if not current.")
4182 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004183 "--print-master", action='store_true', help="print master name as well.")
4184 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004185 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004186 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004187 group.add_option(
4188 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4189 help="Host of buildbucket. The default host is %default.")
4190 parser.add_option_group(group)
4191 auth.add_auth_options(parser)
4192 options, args = parser.parse_args(args)
4193 if args:
4194 parser.error('Unrecognized args: %s' % ' '.join(args))
4195
4196 auth_config = auth.extract_auth_config_from_options(options)
4197 cl = Changelist(auth_config=auth_config)
4198 if not cl.GetIssue():
4199 parser.error('Need to upload first')
4200
4201 if not options.patchset:
4202 options.patchset = cl.GetMostRecentPatchset()
4203 if options.patchset and options.patchset != cl.GetPatchset():
4204 print(
4205 '\nWARNING Mismatch between local config and server. Did a previous '
4206 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4207 'Continuing using\npatchset %s.\n' % options.patchset)
4208 try:
4209 jobs = fetch_try_jobs(auth_config, cl, options)
4210 except BuildbucketResponseException as ex:
4211 print 'Buildbucket error: %s' % ex
4212 return 1
4213 except Exception as e:
4214 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4215 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4216 e, stacktrace)
4217 return 1
4218 print_tryjobs(options, jobs)
4219 return 0
4220
4221
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004222@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004224 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004225 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004226 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004227 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004228
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004229 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004230 if args:
4231 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004232 branch = cl.GetBranch()
4233 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004234 cl = Changelist()
4235 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004236
4237 # Clear configured merge-base, if there is one.
4238 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004239 else:
4240 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004241 return 0
4242
4243
thestig@chromium.org00858c82013-12-02 23:08:03 +00004244def CMDweb(parser, args):
4245 """Opens the current CL in the web browser."""
4246 _, args = parser.parse_args(args)
4247 if args:
4248 parser.error('Unrecognized args: %s' % ' '.join(args))
4249
4250 issue_url = Changelist().GetIssueURL()
4251 if not issue_url:
4252 print >> sys.stderr, 'ERROR No issue to open'
4253 return 1
4254
4255 webbrowser.open(issue_url)
4256 return 0
4257
4258
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004259def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004260 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004261 auth.add_auth_options(parser)
4262 options, args = parser.parse_args(args)
4263 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004264 if args:
4265 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004266 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004267 props = cl.GetIssueProperties()
4268 if props.get('private'):
4269 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004270 cl.SetFlag('commit', '1')
4271 return 0
4272
4273
groby@chromium.org411034a2013-02-26 15:12:01 +00004274def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004275 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004276 auth.add_auth_options(parser)
4277 options, args = parser.parse_args(args)
4278 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004279 if args:
4280 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004281 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004282 # Ensure there actually is an issue to close.
4283 cl.GetDescription()
4284 cl.CloseIssue()
4285 return 0
4286
4287
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004288def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004289 """Shows differences between local tree and last upload."""
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)
4293 if args:
4294 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004295
4296 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004297 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004298 # Staged changes would be committed along with the patch from last
4299 # upload, hence counted toward the "last upload" side in the final
4300 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004301 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004302 return 1
4303
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004304 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004305 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004306 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004307 if not issue:
4308 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004309 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004310 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004311
4312 # Create a new branch based on the merge-base
4313 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
4314 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004315 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004316 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004317 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004318 return rtn
4319
wychen@chromium.org06928532015-02-03 02:11:29 +00004320 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004321 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004322 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004323 finally:
4324 RunGit(['checkout', '-q', branch])
4325 RunGit(['branch', '-D', TMP_BRANCH])
4326
4327 return 0
4328
4329
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004330def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004331 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004332 parser.add_option(
4333 '--no-color',
4334 action='store_true',
4335 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004336 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004337 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004338 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004339
4340 author = RunGit(['config', 'user.email']).strip() or None
4341
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004342 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004343
4344 if args:
4345 if len(args) > 1:
4346 parser.error('Unknown args')
4347 base_branch = args[0]
4348 else:
4349 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004350 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004351
4352 change = cl.GetChange(base_branch, None)
4353 return owners_finder.OwnersFinder(
4354 [f.LocalPath() for f in
4355 cl.GetChange(base_branch, None).AffectedFiles()],
4356 change.RepositoryRoot(), author,
4357 fopen=file, os_path=os.path, glob=glob.glob,
4358 disable_color=options.no_color).run()
4359
4360
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004361def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004362 """Generates a diff command."""
4363 # Generate diff for the current branch's changes.
4364 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4365 upstream_commit, '--' ]
4366
4367 if args:
4368 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004369 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004370 diff_cmd.append(arg)
4371 else:
4372 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004373
4374 return diff_cmd
4375
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004376def MatchingFileType(file_name, extensions):
4377 """Returns true if the file name ends with one of the given extensions."""
4378 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004379
enne@chromium.org555cfe42014-01-29 18:21:39 +00004380@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004381def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004382 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004383 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004384 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004385 parser.add_option('--full', action='store_true',
4386 help='Reformat the full content of all touched files')
4387 parser.add_option('--dry-run', action='store_true',
4388 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004389 parser.add_option('--python', action='store_true',
4390 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004391 parser.add_option('--diff', action='store_true',
4392 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004393 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004394
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004395 # git diff generates paths against the root of the repository. Change
4396 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004397 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004398 if rel_base_path:
4399 os.chdir(rel_base_path)
4400
digit@chromium.org29e47272013-05-17 17:01:46 +00004401 # Grab the merge-base commit, i.e. the upstream commit of the current
4402 # branch when it was created or the last time it was rebased. This is
4403 # to cover the case where the user may have called "git fetch origin",
4404 # moving the origin branch to a newer commit, but hasn't rebased yet.
4405 upstream_commit = None
4406 cl = Changelist()
4407 upstream_branch = cl.GetUpstreamBranch()
4408 if upstream_branch:
4409 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4410 upstream_commit = upstream_commit.strip()
4411
4412 if not upstream_commit:
4413 DieWithError('Could not find base commit for this branch. '
4414 'Are you in detached state?')
4415
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004416 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4417 diff_output = RunGit(changed_files_cmd)
4418 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004419 # Filter out files deleted by this CL
4420 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004421
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004422 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4423 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4424 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004425 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004426
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004427 top_dir = os.path.normpath(
4428 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4429
4430 # Locate the clang-format binary in the checkout
4431 try:
4432 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4433 except clang_format.NotFoundError, e:
4434 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004435
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004436 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4437 # formatted. This is used to block during the presubmit.
4438 return_value = 0
4439
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004440 if clang_diff_files:
4441 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004442 cmd = [clang_format_tool]
4443 if not opts.dry_run and not opts.diff:
4444 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004445 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004446 if opts.diff:
4447 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004448 else:
4449 env = os.environ.copy()
4450 env['PATH'] = str(os.path.dirname(clang_format_tool))
4451 try:
4452 script = clang_format.FindClangFormatScriptInChromiumTree(
4453 'clang-format-diff.py')
4454 except clang_format.NotFoundError, e:
4455 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004456
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004457 cmd = [sys.executable, script, '-p0']
4458 if not opts.dry_run and not opts.diff:
4459 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004460
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004461 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4462 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004463
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004464 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4465 if opts.diff:
4466 sys.stdout.write(stdout)
4467 if opts.dry_run and len(stdout) > 0:
4468 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004469
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004470 # Similar code to above, but using yapf on .py files rather than clang-format
4471 # on C/C++ files
4472 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004473 yapf_tool = gclient_utils.FindExecutable('yapf')
4474 if yapf_tool is None:
4475 DieWithError('yapf not found in PATH')
4476
4477 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004478 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004479 cmd = [yapf_tool]
4480 if not opts.dry_run and not opts.diff:
4481 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004482 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004483 if opts.diff:
4484 sys.stdout.write(stdout)
4485 else:
4486 # TODO(sbc): yapf --lines mode still has some issues.
4487 # https://github.com/google/yapf/issues/154
4488 DieWithError('--python currently only works with --full')
4489
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004490 # Dart's formatter does not have the nice property of only operating on
4491 # modified chunks, so hard code full.
4492 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004493 try:
4494 command = [dart_format.FindDartFmtToolInChromiumTree()]
4495 if not opts.dry_run and not opts.diff:
4496 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004497 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004498
ppi@chromium.org6593d932016-03-03 15:41:15 +00004499 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004500 if opts.dry_run and stdout:
4501 return_value = 2
4502 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004503 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4504 'found in this checkout. Files in other languages are still ' +
4505 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004506
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004507 # Format GN build files. Always run on full build files for canonical form.
4508 if gn_diff_files:
4509 cmd = ['gn', 'format']
4510 if not opts.dry_run and not opts.diff:
4511 cmd.append('--in-place')
4512 for gn_diff_file in gn_diff_files:
4513 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4514 if opts.diff:
4515 sys.stdout.write(stdout)
4516
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004517 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004518
4519
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004520@subcommand.usage('<codereview url or issue id>')
4521def CMDcheckout(parser, args):
4522 """Checks out a branch associated with a given Rietveld issue."""
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004523 # TODO(tandrii): consider adding this for Gerrit?
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004524 _, args = parser.parse_args(args)
4525
4526 if len(args) != 1:
4527 parser.print_help()
4528 return 1
4529
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004530 issue_arg = ParseIssueNumberArgument(args[0])
4531 if issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004532 parser.print_help()
4533 return 1
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004534 target_issue = issue_arg.issue
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004535
4536 key_and_issues = [x.split() for x in RunGit(
4537 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4538 .splitlines()]
4539 branches = []
4540 for key, issue in key_and_issues:
4541 if issue == target_issue:
4542 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4543
4544 if len(branches) == 0:
4545 print 'No branch found for issue %s.' % target_issue
4546 return 1
4547 if len(branches) == 1:
4548 RunGit(['checkout', branches[0]])
4549 else:
4550 print 'Multiple branches match issue %s:' % target_issue
4551 for i in range(len(branches)):
4552 print '%d: %s' % (i, branches[i])
4553 which = raw_input('Choose by index: ')
4554 try:
4555 RunGit(['checkout', branches[int(which)]])
4556 except (IndexError, ValueError):
4557 print 'Invalid selection, not checking out any branch.'
4558 return 1
4559
4560 return 0
4561
4562
maruel@chromium.org29404b52014-09-08 22:58:00 +00004563def CMDlol(parser, args):
4564 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004565 print zlib.decompress(base64.b64decode(
4566 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4567 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4568 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4569 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004570 return 0
4571
4572
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004573class OptionParser(optparse.OptionParser):
4574 """Creates the option parse and add --verbose support."""
4575 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004576 optparse.OptionParser.__init__(
4577 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004578 self.add_option(
4579 '-v', '--verbose', action='count', default=0,
4580 help='Use 2 times for more debugging info')
4581
4582 def parse_args(self, args=None, values=None):
4583 options, args = optparse.OptionParser.parse_args(self, args, values)
4584 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4585 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4586 return options, args
4587
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004588
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004590 if sys.hexversion < 0x02060000:
4591 print >> sys.stderr, (
4592 '\nYour python version %s is unsupported, please upgrade.\n' %
4593 sys.version.split(' ', 1)[0])
4594 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004595
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004596 # Reload settings.
4597 global settings
4598 settings = Settings()
4599
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004600 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004601 dispatcher = subcommand.CommandDispatcher(__name__)
4602 try:
4603 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004604 except auth.AuthenticationError as e:
4605 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004606 except urllib2.HTTPError, e:
4607 if e.code != 500:
4608 raise
4609 DieWithError(
4610 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4611 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004612 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004613
4614
4615if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004616 # These affect sys.stdout so do it outside of main() to simplify mocks in
4617 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004618 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004619 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004620 try:
4621 sys.exit(main(sys.argv[1:]))
4622 except KeyboardInterrupt:
4623 sys.stderr.write('interrupted\n')
4624 sys.exit(1)