blob: 587fe64059e7a4fbb0159e86c912bca1896eae0c [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
maruel@chromium.org0633fb42013-08-16 20:06:14 +000063__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000065DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000066POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000068GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000078# Shortcut since it quickly becomes redundant.
79Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000080
maruel@chromium.orgddd59412011-11-30 14:20:38 +000081# Initialized in main()
82settings = None
83
84
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000086 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 sys.exit(1)
88
89
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000090def GetNoGitPagerEnv():
91 env = os.environ.copy()
92 # 'cat' is a magical git string that disables pagers on all platforms.
93 env['GIT_PAGER'] = 'cat'
94 return env
95
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000096
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000099 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000100 except subprocess2.CalledProcessError as e:
101 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000103 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000104 'Command "%s" failed.\n%s' % (
105 ' '.join(args), error_message or e.stdout or ''))
106 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107
108
109def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000110 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000111 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000115 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000116 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000121 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000122 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000123 stdout=subprocess2.PIPE,
124 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000125 return code, out[0]
126 except ValueError:
127 # When the subprocess fails, it returns None. That triggers a ValueError
128 # when trying to unpack the return value into (out, code).
129 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000130
131
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000132def RunGitSilent(args):
133 """Returns stdout, suppresses stderr and ingores the return code."""
134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000160 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000161 if not branch:
162 return
163
164 cmd = ['config']
165 if isinstance(value, int):
166 cmd.append('--int')
167 git_key = 'branch.%s.%s' % (branch, key)
168 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169
170
171def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000172 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000173 if branch:
174 git_key = 'branch.%s.%s' % (branch, key)
175 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
176 try:
177 return int(stdout.strip())
178 except ValueError:
179 pass
180 return default
181
182
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183def add_git_similarity(parser):
184 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186 help='Sets the percentage that a pair of files need to match in order to'
187 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 parser.add_option(
189 '--find-copies', action='store_true',
190 help='Allows git to look for copies.')
191 parser.add_option(
192 '--no-find-copies', action='store_false', dest='find_copies',
193 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000194
195 old_parser_args = parser.parse_args
196 def Parse(args):
197 options, args = old_parser_args(args)
198
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 print('Note: Saving similarity of %d%% in git config.'
203 % options.similarity)
204 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000205
iannucci@chromium.org79540052012-10-19 23:15:26 +0000206 options.similarity = max(0, min(options.similarity, 100))
207
208 if options.find_copies is None:
209 options.find_copies = bool(
210 git_get_branch_default('git-find-copies', True))
211 else:
212 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000213
214 print('Using %d%% similarity for rename/copy detection. '
215 'Override with --similarity.' % options.similarity)
216
217 return options, args
218 parser.parse_args = Parse
219
220
machenbach@chromium.org45453142015-09-15 08:45:22 +0000221def _get_properties_from_options(options):
222 properties = dict(x.split('=', 1) for x in options.properties)
223 for key, val in properties.iteritems():
224 try:
225 properties[key] = json.loads(val)
226 except ValueError:
227 pass # If a value couldn't be evaluated, treat it as a string.
228 return properties
229
230
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000231def _prefix_master(master):
232 """Convert user-specified master name to full master name.
233
234 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
235 name, while the developers always use shortened master name
236 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
237 function does the conversion for buildbucket migration.
238 """
239 prefix = 'master.'
240 if master.startswith(prefix):
241 return master
242 return '%s%s' % (prefix, master)
243
244
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000245def _buildbucket_retry(operation_name, http, *args, **kwargs):
246 """Retries requests to buildbucket service and returns parsed json content."""
247 try_count = 0
248 while True:
249 response, content = http.request(*args, **kwargs)
250 try:
251 content_json = json.loads(content)
252 except ValueError:
253 content_json = None
254
255 # Buildbucket could return an error even if status==200.
256 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000257 error = content_json.get('error')
258 if error.get('code') == 403:
259 raise BuildbucketResponseException(
260 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000261 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000262 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 raise BuildbucketResponseException(msg)
264
265 if response.status == 200:
266 if not content_json:
267 raise BuildbucketResponseException(
268 'Buildbucket returns invalid json content: %s.\n'
269 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
270 content)
271 return content_json
272 if response.status < 500 or try_count >= 2:
273 raise httplib2.HttpLib2Error(content)
274
275 # status >= 500 means transient failures.
276 logging.debug('Transient errors when %s. Will retry.', operation_name)
277 time.sleep(0.5 + 1.5*try_count)
278 try_count += 1
279 assert False, 'unreachable'
280
281
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000282def trigger_luci_job(changelist, masters, options):
283 """Send a job to run on LUCI."""
284 issue_props = changelist.GetIssueProperties()
285 issue = changelist.GetIssue()
286 patchset = changelist.GetMostRecentPatchset()
287 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000288 # TODO(hinoka et al): add support for other properties.
289 # Currently, this completely ignores testfilter and other properties.
290 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000291 luci_trigger.trigger(
292 builder, 'HEAD', issue, patchset, issue_props['project'])
293
294
machenbach@chromium.org45453142015-09-15 08:45:22 +0000295def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000296 rietveld_url = settings.GetDefaultServerUrl()
297 rietveld_host = urlparse.urlparse(rietveld_url).hostname
298 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
299 http = authenticator.authorize(httplib2.Http())
300 http.force_exception_to_status_code = True
301 issue_props = changelist.GetIssueProperties()
302 issue = changelist.GetIssue()
303 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305
306 buildbucket_put_url = (
307 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000308 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000309 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
310 hostname=rietveld_host,
311 issue=issue,
312 patch=patchset)
313
314 batch_req_body = {'builds': []}
315 print_text = []
316 print_text.append('Tried jobs on:')
317 for master, builders_and_tests in sorted(masters.iteritems()):
318 print_text.append('Master: %s' % master)
319 bucket = _prefix_master(master)
320 for builder, tests in sorted(builders_and_tests.iteritems()):
321 print_text.append(' %s: %s' % (builder, tests))
322 parameters = {
323 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000324 'changes': [{
325 'author': {'email': issue_props['owner_email']},
326 'revision': options.revision,
327 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000328 'properties': {
329 'category': category,
330 'issue': issue,
331 'master': master,
332 'patch_project': issue_props['project'],
333 'patch_storage': 'rietveld',
334 'patchset': patchset,
335 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 },
338 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000339 if tests:
340 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000341 if properties:
342 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000343 if options.clobber:
344 parameters['properties']['clobber'] = True
345 batch_req_body['builds'].append(
346 {
347 'bucket': bucket,
348 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000350 'tags': ['builder:%s' % builder,
351 'buildset:%s' % buildset,
352 'master:%s' % master,
353 'user_agent:git_cl_try']
354 }
355 )
356
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000357 _buildbucket_retry(
358 'triggering tryjobs',
359 http,
360 buildbucket_put_url,
361 'PUT',
362 body=json.dumps(batch_req_body),
363 headers={'Content-Type': 'application/json'}
364 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000365 print_text.append('To see results here, run: git cl try-results')
366 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000367 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000368
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370def fetch_try_jobs(auth_config, changelist, options):
371 """Fetches tryjobs from buildbucket.
372
373 Returns a map from build id to build info as json dictionary.
374 """
375 rietveld_url = settings.GetDefaultServerUrl()
376 rietveld_host = urlparse.urlparse(rietveld_url).hostname
377 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
378 if authenticator.has_cached_credentials():
379 http = authenticator.authorize(httplib2.Http())
380 else:
381 print ('Warning: Some results might be missing because %s' %
382 # Get the message on how to login.
383 auth.LoginRequiredError(rietveld_host).message)
384 http = httplib2.Http()
385
386 http.force_exception_to_status_code = True
387
388 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
389 hostname=rietveld_host,
390 issue=changelist.GetIssue(),
391 patch=options.patchset)
392 params = {'tag': 'buildset:%s' % buildset}
393
394 builds = {}
395 while True:
396 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
397 hostname=options.buildbucket_host,
398 params=urllib.urlencode(params))
399 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
400 for build in content.get('builds', []):
401 builds[build['id']] = build
402 if 'next_cursor' in content:
403 params['start_cursor'] = content['next_cursor']
404 else:
405 break
406 return builds
407
408
409def print_tryjobs(options, builds):
410 """Prints nicely result of fetch_try_jobs."""
411 if not builds:
412 print 'No tryjobs scheduled'
413 return
414
415 # Make a copy, because we'll be modifying builds dictionary.
416 builds = builds.copy()
417 builder_names_cache = {}
418
419 def get_builder(b):
420 try:
421 return builder_names_cache[b['id']]
422 except KeyError:
423 try:
424 parameters = json.loads(b['parameters_json'])
425 name = parameters['builder_name']
426 except (ValueError, KeyError) as error:
427 print 'WARNING: failed to get builder name for build %s: %s' % (
428 b['id'], error)
429 name = None
430 builder_names_cache[b['id']] = name
431 return name
432
433 def get_bucket(b):
434 bucket = b['bucket']
435 if bucket.startswith('master.'):
436 return bucket[len('master.'):]
437 return bucket
438
439 if options.print_master:
440 name_fmt = '%%-%ds %%-%ds' % (
441 max(len(str(get_bucket(b))) for b in builds.itervalues()),
442 max(len(str(get_builder(b))) for b in builds.itervalues()))
443 def get_name(b):
444 return name_fmt % (get_bucket(b), get_builder(b))
445 else:
446 name_fmt = '%%-%ds' % (
447 max(len(str(get_builder(b))) for b in builds.itervalues()))
448 def get_name(b):
449 return name_fmt % get_builder(b)
450
451 def sort_key(b):
452 return b['status'], b.get('result'), get_name(b), b.get('url')
453
454 def pop(title, f, color=None, **kwargs):
455 """Pop matching builds from `builds` dict and print them."""
456
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000457 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000458 colorize = str
459 else:
460 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
461
462 result = []
463 for b in builds.values():
464 if all(b.get(k) == v for k, v in kwargs.iteritems()):
465 builds.pop(b['id'])
466 result.append(b)
467 if result:
468 print colorize(title)
469 for b in sorted(result, key=sort_key):
470 print ' ', colorize('\t'.join(map(str, f(b))))
471
472 total = len(builds)
473 pop(status='COMPLETED', result='SUCCESS',
474 title='Successes:', color=Fore.GREEN,
475 f=lambda b: (get_name(b), b.get('url')))
476 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
477 title='Infra Failures:', color=Fore.MAGENTA,
478 f=lambda b: (get_name(b), b.get('url')))
479 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
480 title='Failures:', color=Fore.RED,
481 f=lambda b: (get_name(b), b.get('url')))
482 pop(status='COMPLETED', result='CANCELED',
483 title='Canceled:', color=Fore.MAGENTA,
484 f=lambda b: (get_name(b),))
485 pop(status='COMPLETED', result='FAILURE',
486 failure_reason='INVALID_BUILD_DEFINITION',
487 title='Wrong master/builder name:', color=Fore.MAGENTA,
488 f=lambda b: (get_name(b),))
489 pop(status='COMPLETED', result='FAILURE',
490 title='Other failures:',
491 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
492 pop(status='COMPLETED',
493 title='Other finished:',
494 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
495 pop(status='STARTED',
496 title='Started:', color=Fore.YELLOW,
497 f=lambda b: (get_name(b), b.get('url')))
498 pop(status='SCHEDULED',
499 title='Scheduled:',
500 f=lambda b: (get_name(b), 'id=%s' % b['id']))
501 # The last section is just in case buildbucket API changes OR there is a bug.
502 pop(title='Other:',
503 f=lambda b: (get_name(b), 'id=%s' % b['id']))
504 assert len(builds) == 0
505 print 'Total: %d tryjobs' % total
506
507
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000508def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
509 """Return the corresponding git ref if |base_url| together with |glob_spec|
510 matches the full |url|.
511
512 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
513 """
514 fetch_suburl, as_ref = glob_spec.split(':')
515 if allow_wildcards:
516 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
517 if glob_match:
518 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
519 # "branches/{472,597,648}/src:refs/remotes/svn/*".
520 branch_re = re.escape(base_url)
521 if glob_match.group(1):
522 branch_re += '/' + re.escape(glob_match.group(1))
523 wildcard = glob_match.group(2)
524 if wildcard == '*':
525 branch_re += '([^/]*)'
526 else:
527 # Escape and replace surrounding braces with parentheses and commas
528 # with pipe symbols.
529 wildcard = re.escape(wildcard)
530 wildcard = re.sub('^\\\\{', '(', wildcard)
531 wildcard = re.sub('\\\\,', '|', wildcard)
532 wildcard = re.sub('\\\\}$', ')', wildcard)
533 branch_re += wildcard
534 if glob_match.group(3):
535 branch_re += re.escape(glob_match.group(3))
536 match = re.match(branch_re, url)
537 if match:
538 return re.sub('\*$', match.group(1), as_ref)
539
540 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
541 if fetch_suburl:
542 full_url = base_url + '/' + fetch_suburl
543 else:
544 full_url = base_url
545 if full_url == url:
546 return as_ref
547 return None
548
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000549
iannucci@chromium.org79540052012-10-19 23:15:26 +0000550def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000551 """Prints statistics about the change to the user."""
552 # --no-ext-diff is broken in some versions of Git, so try to work around
553 # this by overriding the environment (but there is still a problem if the
554 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000555 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000556 if 'GIT_EXTERNAL_DIFF' in env:
557 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000558
559 if find_copies:
560 similarity_options = ['--find-copies-harder', '-l100000',
561 '-C%s' % similarity]
562 else:
563 similarity_options = ['-M%s' % similarity]
564
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000565 try:
566 stdout = sys.stdout.fileno()
567 except AttributeError:
568 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000569 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000570 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000571 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000572 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000573
574
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000575class BuildbucketResponseException(Exception):
576 pass
577
578
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000579class Settings(object):
580 def __init__(self):
581 self.default_server = None
582 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000583 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584 self.is_git_svn = None
585 self.svn_branch = None
586 self.tree_status_url = None
587 self.viewvc_url = None
588 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000589 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000590 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000591 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000592 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000593 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000594 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000595
596 def LazyUpdateIfNeeded(self):
597 """Updates the settings from a codereview.settings file, if available."""
598 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000599 # The only value that actually changes the behavior is
600 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000601 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 error_ok=True
603 ).strip().lower()
604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000606 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 LoadCodereviewSettingsFromFile(cr_settings_file)
608 self.updated = True
609
610 def GetDefaultServerUrl(self, error_ok=False):
611 if not self.default_server:
612 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000614 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 if error_ok:
616 return self.default_server
617 if not self.default_server:
618 error_message = ('Could not find settings file. You must configure '
619 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000621 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.default_server
623
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000624 @staticmethod
625 def GetRelativeRoot():
626 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 if self.root is None:
630 self.root = os.path.abspath(self.GetRelativeRoot())
631 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000633 def GetGitMirror(self, remote='origin'):
634 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000635 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000636 if not os.path.isdir(local_url):
637 return None
638 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
639 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
640 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
641 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
642 if mirror.exists():
643 return mirror
644 return None
645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 def GetIsGitSvn(self):
647 """Return true if this repo looks like it's using git-svn."""
648 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000649 if self.GetPendingRefPrefix():
650 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
651 self.is_git_svn = False
652 else:
653 # If you have any "svn-remote.*" config keys, we think you're using svn.
654 self.is_git_svn = RunGitWithCode(
655 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 return self.is_git_svn
657
658 def GetSVNBranch(self):
659 if self.svn_branch is None:
660 if not self.GetIsGitSvn():
661 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
662
663 # Try to figure out which remote branch we're based on.
664 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000665 # 1) iterate through our branch history and find the svn URL.
666 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000667
668 # regexp matching the git-svn line that contains the URL.
669 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
670
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000671 # We don't want to go through all of history, so read a line from the
672 # pipe at a time.
673 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000674 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000675 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
676 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000677 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000678 for line in proc.stdout:
679 match = git_svn_re.match(line)
680 if match:
681 url = match.group(1)
682 proc.stdout.close() # Cut pipe.
683 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000685 if url:
686 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
687 remotes = RunGit(['config', '--get-regexp',
688 r'^svn-remote\..*\.url']).splitlines()
689 for remote in remotes:
690 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000692 remote = match.group(1)
693 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000694 rewrite_root = RunGit(
695 ['config', 'svn-remote.%s.rewriteRoot' % remote],
696 error_ok=True).strip()
697 if rewrite_root:
698 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000699 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000700 ['config', 'svn-remote.%s.fetch' % remote],
701 error_ok=True).strip()
702 if fetch_spec:
703 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
704 if self.svn_branch:
705 break
706 branch_spec = RunGit(
707 ['config', 'svn-remote.%s.branches' % remote],
708 error_ok=True).strip()
709 if branch_spec:
710 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
711 if self.svn_branch:
712 break
713 tag_spec = RunGit(
714 ['config', 'svn-remote.%s.tags' % remote],
715 error_ok=True).strip()
716 if tag_spec:
717 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
718 if self.svn_branch:
719 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720
721 if not self.svn_branch:
722 DieWithError('Can\'t guess svn branch -- try specifying it on the '
723 'command line')
724
725 return self.svn_branch
726
727 def GetTreeStatusUrl(self, error_ok=False):
728 if not self.tree_status_url:
729 error_message = ('You must configure your tree status URL by running '
730 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000731 self.tree_status_url = self._GetRietveldConfig(
732 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 return self.tree_status_url
734
735 def GetViewVCUrl(self):
736 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 return self.viewvc_url
739
rmistry@google.com90752582014-01-14 21:04:50 +0000740 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000742
rmistry@google.com78948ed2015-07-08 23:09:57 +0000743 def GetIsSkipDependencyUpload(self, branch_name):
744 """Returns true if specified branch should skip dep uploads."""
745 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
746 error_ok=True)
747
rmistry@google.com5626a922015-02-26 14:03:30 +0000748 def GetRunPostUploadHook(self):
749 run_post_upload_hook = self._GetRietveldConfig(
750 'run-post-upload-hook', error_ok=True)
751 return run_post_upload_hook == "True"
752
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000753 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000754 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000755
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000756 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000758
ukai@chromium.orge8077812012-02-03 03:41:46 +0000759 def GetIsGerrit(self):
760 """Return true if this repo is assosiated with gerrit code review system."""
761 if self.is_gerrit is None:
762 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
763 return self.is_gerrit
764
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000765 def GetSquashGerritUploads(self):
766 """Return true if uploads to Gerrit should be squashed by default."""
767 if self.squash_gerrit_uploads is None:
768 self.squash_gerrit_uploads = (
769 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
770 error_ok=True).strip() == 'true')
771 return self.squash_gerrit_uploads
772
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000773 def GetGitEditor(self):
774 """Return the editor specified in the git config, or None if none is."""
775 if self.git_editor is None:
776 self.git_editor = self._GetConfig('core.editor', error_ok=True)
777 return self.git_editor or None
778
thestig@chromium.org44202a22014-03-11 19:22:18 +0000779 def GetLintRegex(self):
780 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
781 DEFAULT_LINT_REGEX)
782
783 def GetLintIgnoreRegex(self):
784 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
785 DEFAULT_LINT_IGNORE_REGEX)
786
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000787 def GetProject(self):
788 if not self.project:
789 self.project = self._GetRietveldConfig('project', error_ok=True)
790 return self.project
791
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000792 def GetForceHttpsCommitUrl(self):
793 if not self.force_https_commit_url:
794 self.force_https_commit_url = self._GetRietveldConfig(
795 'force-https-commit-url', error_ok=True)
796 return self.force_https_commit_url
797
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000798 def GetPendingRefPrefix(self):
799 if not self.pending_ref_prefix:
800 self.pending_ref_prefix = self._GetRietveldConfig(
801 'pending-ref-prefix', error_ok=True)
802 return self.pending_ref_prefix
803
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 def _GetRietveldConfig(self, param, **kwargs):
805 return self._GetConfig('rietveld.' + param, **kwargs)
806
rmistry@google.com78948ed2015-07-08 23:09:57 +0000807 def _GetBranchConfig(self, branch_name, param, **kwargs):
808 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 def _GetConfig(self, param, **kwargs):
811 self.LazyUpdateIfNeeded()
812 return RunGit(['config', param], **kwargs).strip()
813
814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815def ShortBranchName(branch):
816 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000817 return branch.replace('refs/heads/', '', 1)
818
819
820def GetCurrentBranchRef():
821 """Returns branch ref (e.g., refs/heads/master) or None."""
822 return RunGit(['symbolic-ref', 'HEAD'],
823 stderr=subprocess2.VOID, error_ok=True).strip() or None
824
825
826def GetCurrentBranch():
827 """Returns current branch or None.
828
829 For refs/heads/* branches, returns just last part. For others, full ref.
830 """
831 branchref = GetCurrentBranchRef()
832 if branchref:
833 return ShortBranchName(branchref)
834 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
836
837class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000838 """Changelist works with one changelist in local branch.
839
840 Supports two codereview backends: Rietveld or Gerrit, selected at object
841 creation.
842
843 Not safe for concurrent multi-{thread,process} use.
844 """
845
846 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
847 """Create a new ChangeList instance.
848
849 If issue is given, the codereview must be given too.
850
851 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
852 Otherwise, it's decided based on current configuration of the local branch,
853 with default being 'rietveld' for backwards compatibility.
854 See _load_codereview_impl for more details.
855
856 **kwargs will be passed directly to codereview implementation.
857 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000859 global settings
860 if not settings:
861 # Happens when git_cl.py is used as a utility library.
862 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000863
864 if issue:
865 assert codereview, 'codereview must be known, if issue is known'
866
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 self.branchref = branchref
868 if self.branchref:
869 self.branch = ShortBranchName(self.branchref)
870 else:
871 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000873 self.lookedup_issue = False
874 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 self.has_description = False
876 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000877 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000879 self.cc = None
880 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000881 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000882
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000883 self._codereview_impl = None
884 self._load_codereview_impl(codereview, **kwargs)
885
886 def _load_codereview_impl(self, codereview=None, **kwargs):
887 if codereview:
888 codereview = codereview.lower()
889 if codereview == 'gerrit':
890 self._codereview_impl = _GerritChangelistImpl(self, **kwargs)
891 elif codereview == 'rietveld':
892 self._codereview_impl = _RietveldChangelistImpl(self, **kwargs)
893 else:
894 assert codereview in ('rietveld', 'gerrit')
895 return
896
897 # Automatic selection based on issue number set for a current branch.
898 # Rietveld takes precedence over Gerrit.
899 assert not self.issue
900 # Whether we find issue or not, we are doing the lookup.
901 self.lookedup_issue = True
902 for cls in [_RietveldChangelistImpl, _GerritChangelistImpl]:
903 setting = cls.IssueSetting(self.GetBranch())
904 issue = RunGit(['config', setting], error_ok=True).strip()
905 if issue:
906 self._codereview_impl = cls(self, **kwargs)
907 self.issue = int(issue)
908 return
909
910 # No issue is set for this branch, so decide based on repo-wide settings.
911 return self._load_codereview_impl(
912 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
913 **kwargs)
914
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000915
916 def GetCCList(self):
917 """Return the users cc'd on this CL.
918
919 Return is a string suitable for passing to gcl with the --cc flag.
920 """
921 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000922 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000923 more_cc = ','.join(self.watchers)
924 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
925 return self.cc
926
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000927 def GetCCListWithoutDefault(self):
928 """Return the users cc'd on this CL excluding default ones."""
929 if self.cc is None:
930 self.cc = ','.join(self.watchers)
931 return self.cc
932
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000933 def SetWatchers(self, watchers):
934 """Set the list of email addresses that should be cc'd based on the changed
935 files in this CL.
936 """
937 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938
939 def GetBranch(self):
940 """Returns the short branch name, e.g. 'master'."""
941 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000942 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000943 if not branchref:
944 return None
945 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 self.branch = ShortBranchName(self.branchref)
947 return self.branch
948
949 def GetBranchRef(self):
950 """Returns the full branch name, e.g. 'refs/heads/master'."""
951 self.GetBranch() # Poke the lazy loader.
952 return self.branchref
953
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000954 @staticmethod
955 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000956 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 e.g. 'origin', 'refs/heads/master'
958 """
959 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
961 error_ok=True).strip()
962 if upstream_branch:
963 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
964 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000965 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
966 error_ok=True).strip()
967 if upstream_branch:
968 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000970 # Fall back on trying a git-svn upstream branch.
971 if settings.GetIsGitSvn():
972 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000974 # Else, try to guess the origin remote.
975 remote_branches = RunGit(['branch', '-r']).split()
976 if 'origin/master' in remote_branches:
977 # Fall back on origin/master if it exits.
978 remote = 'origin'
979 upstream_branch = 'refs/heads/master'
980 elif 'origin/trunk' in remote_branches:
981 # Fall back on origin/trunk if it exists. Generally a shared
982 # git-svn clone
983 remote = 'origin'
984 upstream_branch = 'refs/heads/trunk'
985 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000986 DieWithError(
987 'Unable to determine default branch to diff against.\n'
988 'Either pass complete "git diff"-style arguments, like\n'
989 ' git cl upload origin/master\n'
990 'or verify this branch is set up to track another \n'
991 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992
993 return remote, upstream_branch
994
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000995 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000996 upstream_branch = self.GetUpstreamBranch()
997 if not BranchExists(upstream_branch):
998 DieWithError('The upstream for the current branch (%s) does not exist '
999 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001000 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001001 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003 def GetUpstreamBranch(self):
1004 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001005 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001007 upstream_branch = upstream_branch.replace('refs/heads/',
1008 'refs/remotes/%s/' % remote)
1009 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1010 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 self.upstream_branch = upstream_branch
1012 return self.upstream_branch
1013
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001014 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001015 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001016 remote, branch = None, self.GetBranch()
1017 seen_branches = set()
1018 while branch not in seen_branches:
1019 seen_branches.add(branch)
1020 remote, branch = self.FetchUpstreamTuple(branch)
1021 branch = ShortBranchName(branch)
1022 if remote != '.' or branch.startswith('refs/remotes'):
1023 break
1024 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001025 remotes = RunGit(['remote'], error_ok=True).split()
1026 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001027 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001028 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001029 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001030 logging.warning('Could not determine which remote this change is '
1031 'associated with, so defaulting to "%s". This may '
1032 'not be what you want. You may prevent this message '
1033 'by running "git svn info" as documented here: %s',
1034 self._remote,
1035 GIT_INSTRUCTIONS_URL)
1036 else:
1037 logging.warn('Could not determine which remote this change is '
1038 'associated with. You may prevent this message by '
1039 'running "git svn info" as documented here: %s',
1040 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001041 branch = 'HEAD'
1042 if branch.startswith('refs/remotes'):
1043 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001044 elif branch.startswith('refs/branch-heads/'):
1045 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001046 else:
1047 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001048 return self._remote
1049
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 def GitSanityChecks(self, upstream_git_obj):
1051 """Checks git repo status and ensures diff is from local commits."""
1052
sbc@chromium.org79706062015-01-14 21:18:12 +00001053 if upstream_git_obj is None:
1054 if self.GetBranch() is None:
1055 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001056 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001057 else:
1058 print >> sys.stderr, (
1059 'ERROR: no upstream branch')
1060 return False
1061
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001062 # Verify the commit we're diffing against is in our current branch.
1063 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1064 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1065 if upstream_sha != common_ancestor:
1066 print >> sys.stderr, (
1067 'ERROR: %s is not in the current branch. You may need to rebase '
1068 'your tracking branch' % upstream_sha)
1069 return False
1070
1071 # List the commits inside the diff, and verify they are all local.
1072 commits_in_diff = RunGit(
1073 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1074 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1075 remote_branch = remote_branch.strip()
1076 if code != 0:
1077 _, remote_branch = self.GetRemoteBranch()
1078
1079 commits_in_remote = RunGit(
1080 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1081
1082 common_commits = set(commits_in_diff) & set(commits_in_remote)
1083 if common_commits:
1084 print >> sys.stderr, (
1085 'ERROR: Your diff contains %d commits already in %s.\n'
1086 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1087 'the diff. If you are using a custom git flow, you can override'
1088 ' the reference used for this check with "git config '
1089 'gitcl.remotebranch <git-ref>".' % (
1090 len(common_commits), remote_branch, upstream_git_obj))
1091 return False
1092 return True
1093
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001094 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001095 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001096
1097 Returns None if it is not set.
1098 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001099 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1100 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001101
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001102 def GetGitSvnRemoteUrl(self):
1103 """Return the configured git-svn remote URL parsed from git svn info.
1104
1105 Returns None if it is not set.
1106 """
1107 # URL is dependent on the current directory.
1108 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1109 if data:
1110 keys = dict(line.split(': ', 1) for line in data.splitlines()
1111 if ': ' in line)
1112 return keys.get('URL', None)
1113 return None
1114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 def GetRemoteUrl(self):
1116 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1117
1118 Returns None if there is no remote.
1119 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001120 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001121 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1122
1123 # If URL is pointing to a local directory, it is probably a git cache.
1124 if os.path.isdir(url):
1125 url = RunGit(['config', 'remote.%s.url' % remote],
1126 error_ok=True,
1127 cwd=url).strip()
1128 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001130 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001131 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001132 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001133 issue = RunGit(['config',
1134 self._codereview_impl.IssueSetting(self.GetBranch())],
1135 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 self.issue = int(issue) or None if issue else None
1137 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 return self.issue
1139
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 def GetIssueURL(self):
1141 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142 issue = self.GetIssue()
1143 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001144 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146
1147 def GetDescription(self, pretty=False):
1148 if not self.has_description:
1149 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001150 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 self.has_description = True
1152 if pretty:
1153 wrapper = textwrap.TextWrapper()
1154 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1155 return wrapper.fill(self.description)
1156 return self.description
1157
1158 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001159 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001161 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001163 self.patchset = int(patchset) or None if patchset else None
1164 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 return self.patchset
1166
1167 def SetPatchset(self, patchset):
1168 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001169 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001171 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001172 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001175 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001176 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001178 def SetIssue(self, issue=None):
1179 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001180 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1181 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001183 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001184 RunGit(['config', issue_setting, str(issue)])
1185 codereview_server = self._codereview_impl.GetCodereviewServer()
1186 if codereview_server:
1187 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001189 current_issue = self.GetIssue()
1190 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001191 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001192 self.issue = None
1193 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001195 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001196 if not self.GitSanityChecks(upstream_branch):
1197 DieWithError('\nGit sanity check failure')
1198
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001200 if not root:
1201 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001202 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001203
1204 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001205 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001206 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001207 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001208 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001209 except subprocess2.CalledProcessError:
1210 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001211 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001212 'This branch probably doesn\'t exist anymore. To reset the\n'
1213 'tracking branch, please run\n'
1214 ' git branch --set-upstream %s trunk\n'
1215 'replacing trunk with origin/master or the relevant branch') %
1216 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001217
maruel@chromium.org52424302012-08-29 15:14:30 +00001218 issue = self.GetIssue()
1219 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001220 if issue:
1221 description = self.GetDescription()
1222 else:
1223 # If the change was never uploaded, use the log messages of all commits
1224 # up to the branch point, as git cl upload will prefill the description
1225 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001226 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1227 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001228
1229 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001230 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001231 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001232 name,
1233 description,
1234 absroot,
1235 files,
1236 issue,
1237 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001238 author,
1239 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001240
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001241 def UpdateDescription(self, description):
1242 self.description = description
1243 return self._codereview_impl.UpdateDescriptionRemote(description)
1244
1245 def RunHook(self, committing, may_prompt, verbose, change):
1246 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1247 try:
1248 return presubmit_support.DoPresubmitChecks(change, committing,
1249 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1250 default_presubmit=None, may_prompt=may_prompt,
1251 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1252 except presubmit_support.PresubmitFailure, e:
1253 DieWithError(
1254 ('%s\nMaybe your depot_tools is out of date?\n'
1255 'If all fails, contact maruel@') % e)
1256
1257 # Forward methods to codereview specific implementation.
1258
1259 def CloseIssue(self):
1260 return self._codereview_impl.CloseIssue()
1261
1262 def GetStatus(self):
1263 return self._codereview_impl.GetStatus()
1264
1265 def GetCodereviewServer(self):
1266 return self._codereview_impl.GetCodereviewServer()
1267
1268 def GetApprovingReviewers(self):
1269 return self._codereview_impl.GetApprovingReviewers()
1270
1271 def GetMostRecentPatchset(self):
1272 return self._codereview_impl.GetMostRecentPatchset()
1273
1274 def __getattr__(self, attr):
1275 # This is because lots of untested code accesses Rietveld-specific stuff
1276 # directly, and it's hard to fix for sure. So, just let it work, and fix
1277 # on a cases by case basis.
1278 return getattr(self._codereview_impl, attr)
1279
1280
1281class _ChangelistCodereviewBase(object):
1282 """Abstract base class encapsulating codereview specifics of a changelist."""
1283 def __init__(self, changelist):
1284 self._changelist = changelist # instance of Changelist
1285
1286 def __getattr__(self, attr):
1287 # Forward methods to changelist.
1288 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1289 # _RietveldChangelistImpl to avoid this hack?
1290 return getattr(self._changelist, attr)
1291
1292 def GetStatus(self):
1293 """Apply a rough heuristic to give a simple summary of an issue's review
1294 or CQ status, assuming adherence to a common workflow.
1295
1296 Returns None if no issue for this branch, or specific string keywords.
1297 """
1298 raise NotImplementedError()
1299
1300 def GetCodereviewServer(self):
1301 """Returns server URL without end slash, like "https://codereview.com"."""
1302 raise NotImplementedError()
1303
1304 def FetchDescription(self):
1305 """Fetches and returns description from the codereview server."""
1306 raise NotImplementedError()
1307
1308 def GetCodereviewServerSetting(self):
1309 """Returns git config setting for the codereview server."""
1310 raise NotImplementedError()
1311
1312 @staticmethod
1313 def IssueSetting(branch):
1314 """Returns name of git config setting which stores issue number for a given
1315 branch."""
1316 raise NotImplementedError()
1317
1318 def PatchsetSetting(self):
1319 """Returns name of git config setting which stores issue number."""
1320 raise NotImplementedError()
1321
1322 def GetRieveldObjForPresubmit(self):
1323 # This is an unfortunate Rietveld-embeddedness in presubmit.
1324 # For non-Rietveld codereviews, this probably should return a dummy object.
1325 raise NotImplementedError()
1326
1327 def UpdateDescriptionRemote(self, description):
1328 """Update the description on codereview site."""
1329 raise NotImplementedError()
1330
1331 def CloseIssue(self):
1332 """Closes the issue."""
1333 raise NotImplementedError()
1334
1335 def GetApprovingReviewers(self):
1336 """Returns a list of reviewers approving the change.
1337
1338 Note: not necessarily committers.
1339 """
1340 raise NotImplementedError()
1341
1342 def GetMostRecentPatchset(self):
1343 """Returns the most recent patchset number from the codereview site."""
1344 raise NotImplementedError()
1345
1346
1347class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1348 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1349 super(_RietveldChangelistImpl, self).__init__(changelist)
1350 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1351 settings.GetDefaultServerUrl()
1352
1353 self._rietveld_server = rietveld_server
1354 self._auth_config = auth_config
1355 self._props = None
1356 self._rpc_server = None
1357
1358 def GetAuthConfig(self):
1359 return self._auth_config
1360
1361 def GetCodereviewServer(self):
1362 if not self._rietveld_server:
1363 # If we're on a branch then get the server potentially associated
1364 # with that branch.
1365 if self.GetIssue():
1366 rietveld_server_setting = self.GetCodereviewServerSetting()
1367 if rietveld_server_setting:
1368 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1369 ['config', rietveld_server_setting], error_ok=True).strip())
1370 if not self._rietveld_server:
1371 self._rietveld_server = settings.GetDefaultServerUrl()
1372 return self._rietveld_server
1373
1374 def FetchDescription(self):
1375 issue = self.GetIssue()
1376 assert issue
1377 try:
1378 return self.RpcServer().get_description(issue).strip()
1379 except urllib2.HTTPError as e:
1380 if e.code == 404:
1381 DieWithError(
1382 ('\nWhile fetching the description for issue %d, received a '
1383 '404 (not found)\n'
1384 'error. It is likely that you deleted this '
1385 'issue on the server. If this is the\n'
1386 'case, please run\n\n'
1387 ' git cl issue 0\n\n'
1388 'to clear the association with the deleted issue. Then run '
1389 'this command again.') % issue)
1390 else:
1391 DieWithError(
1392 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1393 except urllib2.URLError as e:
1394 print >> sys.stderr, (
1395 'Warning: Failed to retrieve CL description due to network '
1396 'failure.')
1397 return ''
1398
1399 def GetMostRecentPatchset(self):
1400 return self.GetIssueProperties()['patchsets'][-1]
1401
1402 def GetPatchSetDiff(self, issue, patchset):
1403 return self.RpcServer().get(
1404 '/download/issue%s_%s.diff' % (issue, patchset))
1405
1406 def GetIssueProperties(self):
1407 if self._props is None:
1408 issue = self.GetIssue()
1409 if not issue:
1410 self._props = {}
1411 else:
1412 self._props = self.RpcServer().get_issue_properties(issue, True)
1413 return self._props
1414
1415 def GetApprovingReviewers(self):
1416 return get_approving_reviewers(self.GetIssueProperties())
1417
1418 def AddComment(self, message):
1419 return self.RpcServer().add_comment(self.GetIssue(), message)
1420
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001421 def GetStatus(self):
1422 """Apply a rough heuristic to give a simple summary of an issue's review
1423 or CQ status, assuming adherence to a common workflow.
1424
1425 Returns None if no issue for this branch, or one of the following keywords:
1426 * 'error' - error from review tool (including deleted issues)
1427 * 'unsent' - not sent for review
1428 * 'waiting' - waiting for review
1429 * 'reply' - waiting for owner to reply to review
1430 * 'lgtm' - LGTM from at least one approved reviewer
1431 * 'commit' - in the commit queue
1432 * 'closed' - closed
1433 """
1434 if not self.GetIssue():
1435 return None
1436
1437 try:
1438 props = self.GetIssueProperties()
1439 except urllib2.HTTPError:
1440 return 'error'
1441
1442 if props.get('closed'):
1443 # Issue is closed.
1444 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001445 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001446 # Issue is in the commit queue.
1447 return 'commit'
1448
1449 try:
1450 reviewers = self.GetApprovingReviewers()
1451 except urllib2.HTTPError:
1452 return 'error'
1453
1454 if reviewers:
1455 # Was LGTM'ed.
1456 return 'lgtm'
1457
1458 messages = props.get('messages') or []
1459
1460 if not messages:
1461 # No message was sent.
1462 return 'unsent'
1463 if messages[-1]['sender'] != props.get('owner_email'):
1464 # Non-LGTM reply from non-owner
1465 return 'reply'
1466 return 'waiting'
1467
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001468 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001469 return self.RpcServer().update_description(
1470 self.GetIssue(), self.description)
1471
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001473 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001475 def SetFlag(self, flag, value):
1476 """Patchset must match."""
1477 if not self.GetPatchset():
1478 DieWithError('The patchset needs to match. Send another patchset.')
1479 try:
1480 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001481 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001482 except urllib2.HTTPError, e:
1483 if e.code == 404:
1484 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1485 if e.code == 403:
1486 DieWithError(
1487 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1488 'match?') % (self.GetIssue(), self.GetPatchset()))
1489 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001491 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 """Returns an upload.RpcServer() to access this review's rietveld instance.
1493 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001494 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001495 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001497 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001498 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001499
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001500 @staticmethod
1501 def IssueSetting(branch):
1502 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001504 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505 """Return the git setting that stores this change's most recent patchset."""
1506 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1507
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001508 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001509 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001510 branch = self.GetBranch()
1511 if branch:
1512 return 'branch.%s.rietveldserver' % branch
1513 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001514
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001515 def GetRieveldObjForPresubmit(self):
1516 return self.RpcServer()
1517
1518
1519class _GerritChangelistImpl(_ChangelistCodereviewBase):
1520 def __init__(self, changelist, auth_config=None):
1521 # auth_config is Rietveld thing, kept here to preserve interface only.
1522 super(_GerritChangelistImpl, self).__init__(changelist)
1523 self._change_id = None
1524 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1525 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1526
1527 def _GetGerritHost(self):
1528 # Lazy load of configs.
1529 self.GetCodereviewServer()
1530 return self._gerrit_host
1531
1532 def GetCodereviewServer(self):
1533 if not self._gerrit_server:
1534 # If we're on a branch then get the server potentially associated
1535 # with that branch.
1536 if self.GetIssue():
1537 gerrit_server_setting = self.GetCodereviewServerSetting()
1538 if gerrit_server_setting:
1539 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1540 error_ok=True).strip()
1541 if self._gerrit_server:
1542 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1543 if not self._gerrit_server:
1544 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1545 # has "-review" suffix for lowest level subdomain.
1546 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1547 parts[0] = parts[0] + '-review'
1548 self._gerrit_host = '.'.join(parts)
1549 self._gerrit_server = 'https://%s' % self._gerrit_host
1550 return self._gerrit_server
1551
1552 @staticmethod
1553 def IssueSetting(branch):
1554 return 'branch.%s.gerritissue' % branch
1555
1556 def PatchsetSetting(self):
1557 """Return the git setting that stores this change's most recent patchset."""
1558 return 'branch.%s.gerritpatchset' % self.GetBranch()
1559
1560 def GetCodereviewServerSetting(self):
1561 """Returns the git setting that stores this change's Gerrit server."""
1562 branch = self.GetBranch()
1563 if branch:
1564 return 'branch.%s.gerritserver' % branch
1565 return None
1566
1567 def GetRieveldObjForPresubmit(self):
1568 class ThisIsNotRietveldIssue(object):
1569 def __nonzero__(self):
1570 # This is a hack to make presubmit_support think that rietveld is not
1571 # defined, yet still ensure that calls directly result in a decent
1572 # exception message below.
1573 return False
1574
1575 def __getattr__(self, attr):
1576 print(
1577 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1578 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1579 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1580 'or use Rietveld for codereview.\n'
1581 'See also http://crbug.com/579160.' % attr)
1582 raise NotImplementedError()
1583 return ThisIsNotRietveldIssue()
1584
1585 def GetStatus(self):
1586 # TODO(tandrii)
1587 raise NotImplementedError()
1588
1589 def GetMostRecentPatchset(self):
1590 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1591 ['CURRENT_REVISION'])
1592 return data['revisions'][data['current_revision']]['_number']
1593
1594 def FetchDescription(self):
1595 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1596 ['COMMIT_FOOTERS', 'CURRENT_REVISION'])
1597 return data['revisions'][data['current_revision']]['commit_with_footers']
1598
1599 def UpdateDescriptionRemote(self, description):
1600 # TODO(tandrii)
1601 raise NotImplementedError()
1602
1603 def CloseIssue(self):
1604 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
1605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001606
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001607class ChangeDescription(object):
1608 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001609 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001610 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001611
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001612 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001613 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001614
agable@chromium.org42c20792013-09-12 17:34:49 +00001615 @property # www.logilab.org/ticket/89786
1616 def description(self): # pylint: disable=E0202
1617 return '\n'.join(self._description_lines)
1618
1619 def set_description(self, desc):
1620 if isinstance(desc, basestring):
1621 lines = desc.splitlines()
1622 else:
1623 lines = [line.rstrip() for line in desc]
1624 while lines and not lines[0]:
1625 lines.pop(0)
1626 while lines and not lines[-1]:
1627 lines.pop(-1)
1628 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001629
piman@chromium.org336f9122014-09-04 02:16:55 +00001630 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001631 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001632 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001633 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001634 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001635 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001636
agable@chromium.org42c20792013-09-12 17:34:49 +00001637 # Get the set of R= and TBR= lines and remove them from the desciption.
1638 regexp = re.compile(self.R_LINE)
1639 matches = [regexp.match(line) for line in self._description_lines]
1640 new_desc = [l for i, l in enumerate(self._description_lines)
1641 if not matches[i]]
1642 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001643
agable@chromium.org42c20792013-09-12 17:34:49 +00001644 # Construct new unified R= and TBR= lines.
1645 r_names = []
1646 tbr_names = []
1647 for match in matches:
1648 if not match:
1649 continue
1650 people = cleanup_list([match.group(2).strip()])
1651 if match.group(1) == 'TBR':
1652 tbr_names.extend(people)
1653 else:
1654 r_names.extend(people)
1655 for name in r_names:
1656 if name not in reviewers:
1657 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001658 if add_owners_tbr:
1659 owners_db = owners.Database(change.RepositoryRoot(),
1660 fopen=file, os_path=os.path, glob=glob.glob)
1661 all_reviewers = set(tbr_names + reviewers)
1662 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1663 all_reviewers)
1664 tbr_names.extend(owners_db.reviewers_for(missing_files,
1665 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001666 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1667 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1668
1669 # Put the new lines in the description where the old first R= line was.
1670 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1671 if 0 <= line_loc < len(self._description_lines):
1672 if new_tbr_line:
1673 self._description_lines.insert(line_loc, new_tbr_line)
1674 if new_r_line:
1675 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001676 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001677 if new_r_line:
1678 self.append_footer(new_r_line)
1679 if new_tbr_line:
1680 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001681
1682 def prompt(self):
1683 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001684 self.set_description([
1685 '# Enter a description of the change.',
1686 '# This will be displayed on the codereview site.',
1687 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001688 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001689 '--------------------',
1690 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001691
agable@chromium.org42c20792013-09-12 17:34:49 +00001692 regexp = re.compile(self.BUG_LINE)
1693 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001694 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001695 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001696 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001697 if not content:
1698 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001699 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001700
1701 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001702 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1703 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001704 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001705 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001706
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001707 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001708 if self._description_lines:
1709 # Add an empty line if either the last line or the new line isn't a tag.
1710 last_line = self._description_lines[-1]
1711 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1712 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1713 self._description_lines.append('')
1714 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001715
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001716 def get_reviewers(self):
1717 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001718 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1719 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001720 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001721
1722
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001723def get_approving_reviewers(props):
1724 """Retrieves the reviewers that approved a CL from the issue properties with
1725 messages.
1726
1727 Note that the list may contain reviewers that are not committer, thus are not
1728 considered by the CQ.
1729 """
1730 return sorted(
1731 set(
1732 message['sender']
1733 for message in props['messages']
1734 if message['approval'] and message['sender'] in props['reviewers']
1735 )
1736 )
1737
1738
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739def FindCodereviewSettingsFile(filename='codereview.settings'):
1740 """Finds the given file starting in the cwd and going up.
1741
1742 Only looks up to the top of the repository unless an
1743 'inherit-review-settings-ok' file exists in the root of the repository.
1744 """
1745 inherit_ok_file = 'inherit-review-settings-ok'
1746 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001747 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001748 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1749 root = '/'
1750 while True:
1751 if filename in os.listdir(cwd):
1752 if os.path.isfile(os.path.join(cwd, filename)):
1753 return open(os.path.join(cwd, filename))
1754 if cwd == root:
1755 break
1756 cwd = os.path.dirname(cwd)
1757
1758
1759def LoadCodereviewSettingsFromFile(fileobj):
1760 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001761 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763 def SetProperty(name, setting, unset_error_ok=False):
1764 fullname = 'rietveld.' + name
1765 if setting in keyvals:
1766 RunGit(['config', fullname, keyvals[setting]])
1767 else:
1768 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1769
1770 SetProperty('server', 'CODE_REVIEW_SERVER')
1771 # Only server setting is required. Other settings can be absent.
1772 # In that case, we ignore errors raised during option deletion attempt.
1773 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001774 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001775 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1776 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001777 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001778 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001779 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1780 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001781 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001782 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001783 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001784 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1785 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001787 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001788 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001789
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001790 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1791 RunGit(['config', 'gerrit.squash-uploads',
1792 keyvals['GERRIT_SQUASH_UPLOADS']])
1793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1795 #should be of the form
1796 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1797 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1798 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1799 keyvals['ORIGIN_URL_CONFIG']])
1800
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001802def urlretrieve(source, destination):
1803 """urllib is broken for SSL connections via a proxy therefore we
1804 can't use urllib.urlretrieve()."""
1805 with open(destination, 'w') as f:
1806 f.write(urllib2.urlopen(source).read())
1807
1808
ukai@chromium.org712d6102013-11-27 00:52:58 +00001809def hasSheBang(fname):
1810 """Checks fname is a #! script."""
1811 with open(fname) as f:
1812 return f.read(2).startswith('#!')
1813
1814
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001815def DownloadGerritHook(force):
1816 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001817
1818 Args:
1819 force: True to update hooks. False to install hooks if not present.
1820 """
1821 if not settings.GetIsGerrit():
1822 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001823 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001824 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1825 if not os.access(dst, os.X_OK):
1826 if os.path.exists(dst):
1827 if not force:
1828 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001829 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001830 print(
1831 'WARNING: installing Gerrit commit-msg hook.\n'
1832 ' This behavior of git cl will soon be disabled.\n'
1833 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001834 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001835 if not hasSheBang(dst):
1836 DieWithError('Not a script: %s\n'
1837 'You need to download from\n%s\n'
1838 'into .git/hooks/commit-msg and '
1839 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001840 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1841 except Exception:
1842 if os.path.exists(dst):
1843 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001844 DieWithError('\nFailed to download hooks.\n'
1845 'You need to download from\n%s\n'
1846 'into .git/hooks/commit-msg and '
1847 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001848
1849
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001850
1851def GetRietveldCodereviewSettingsInteractively():
1852 """Prompt the user for settings."""
1853 server = settings.GetDefaultServerUrl(error_ok=True)
1854 prompt = 'Rietveld server (host[:port])'
1855 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1856 newserver = ask_for_data(prompt + ':')
1857 if not server and not newserver:
1858 newserver = DEFAULT_SERVER
1859 if newserver:
1860 newserver = gclient_utils.UpgradeToHttps(newserver)
1861 if newserver != server:
1862 RunGit(['config', 'rietveld.server', newserver])
1863
1864 def SetProperty(initial, caption, name, is_url):
1865 prompt = caption
1866 if initial:
1867 prompt += ' ("x" to clear) [%s]' % initial
1868 new_val = ask_for_data(prompt + ':')
1869 if new_val == 'x':
1870 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1871 elif new_val:
1872 if is_url:
1873 new_val = gclient_utils.UpgradeToHttps(new_val)
1874 if new_val != initial:
1875 RunGit(['config', 'rietveld.' + name, new_val])
1876
1877 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1878 SetProperty(settings.GetDefaultPrivateFlag(),
1879 'Private flag (rietveld only)', 'private', False)
1880 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1881 'tree-status-url', False)
1882 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1883 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1884 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1885 'run-post-upload-hook', False)
1886
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001887@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001888def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001889 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001890
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001891 print('WARNING: git cl config works for Rietveld only.\n'
1892 'For Gerrit, see http://crbug.com/579160.')
1893 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001894 parser.add_option('--activate-update', action='store_true',
1895 help='activate auto-updating [rietveld] section in '
1896 '.git/config')
1897 parser.add_option('--deactivate-update', action='store_true',
1898 help='deactivate auto-updating [rietveld] section in '
1899 '.git/config')
1900 options, args = parser.parse_args(args)
1901
1902 if options.deactivate_update:
1903 RunGit(['config', 'rietveld.autoupdate', 'false'])
1904 return
1905
1906 if options.activate_update:
1907 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1908 return
1909
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001911 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912 return 0
1913
1914 url = args[0]
1915 if not url.endswith('codereview.settings'):
1916 url = os.path.join(url, 'codereview.settings')
1917
1918 # Load code review settings and download hooks (if available).
1919 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1920 return 0
1921
1922
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001923def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001924 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001925 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1926 branch = ShortBranchName(branchref)
1927 _, args = parser.parse_args(args)
1928 if not args:
1929 print("Current base-url:")
1930 return RunGit(['config', 'branch.%s.base-url' % branch],
1931 error_ok=False).strip()
1932 else:
1933 print("Setting base-url to %s" % args[0])
1934 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1935 error_ok=False).strip()
1936
1937
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001938def color_for_status(status):
1939 """Maps a Changelist status to color, for CMDstatus and other tools."""
1940 return {
1941 'unsent': Fore.RED,
1942 'waiting': Fore.BLUE,
1943 'reply': Fore.YELLOW,
1944 'lgtm': Fore.GREEN,
1945 'commit': Fore.MAGENTA,
1946 'closed': Fore.CYAN,
1947 'error': Fore.WHITE,
1948 }.get(status, Fore.WHITE)
1949
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001950def fetch_cl_status(branch, auth_config=None):
1951 """Fetches information for an issue and returns (branch, issue, status)."""
1952 cl = Changelist(branchref=branch, auth_config=auth_config)
1953 url = cl.GetIssueURL()
1954 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001955
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001956 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001957 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001958 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001959
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001960 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001961
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001962def get_cl_statuses(
1963 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001964 """Returns a blocking iterable of (branch, issue, color) for given branches.
1965
1966 If fine_grained is true, this will fetch CL statuses from the server.
1967 Otherwise, simply indicate if there's a matching url for the given branches.
1968
1969 If max_processes is specified, it is used as the maximum number of processes
1970 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1971 spawned.
1972 """
1973 # Silence upload.py otherwise it becomes unwieldly.
1974 upload.verbosity = 0
1975
1976 if fine_grained:
1977 # Process one branch synchronously to work through authentication, then
1978 # spawn processes to process all the other branches in parallel.
1979 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001980 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1981 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001982
1983 branches_to_fetch = branches[1:]
1984 pool = ThreadPool(
1985 min(max_processes, len(branches_to_fetch))
1986 if max_processes is not None
1987 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001988 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001989 yield x
1990 else:
1991 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1992 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001993 cl = Changelist(branchref=b, auth_config=auth_config)
1994 url = cl.GetIssueURL()
1995 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001996
rmistry@google.com2dd99862015-06-22 12:22:18 +00001997
1998def upload_branch_deps(cl, args):
1999 """Uploads CLs of local branches that are dependents of the current branch.
2000
2001 If the local branch dependency tree looks like:
2002 test1 -> test2.1 -> test3.1
2003 -> test3.2
2004 -> test2.2 -> test3.3
2005
2006 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2007 run on the dependent branches in this order:
2008 test2.1, test3.1, test3.2, test2.2, test3.3
2009
2010 Note: This function does not rebase your local dependent branches. Use it when
2011 you make a change to the parent branch that will not conflict with its
2012 dependent branches, and you would like their dependencies updated in
2013 Rietveld.
2014 """
2015 if git_common.is_dirty_git_tree('upload-branch-deps'):
2016 return 1
2017
2018 root_branch = cl.GetBranch()
2019 if root_branch is None:
2020 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2021 'Get on a branch!')
2022 if not cl.GetIssue() or not cl.GetPatchset():
2023 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2024 'patchset dependencies without an uploaded CL.')
2025
2026 branches = RunGit(['for-each-ref',
2027 '--format=%(refname:short) %(upstream:short)',
2028 'refs/heads'])
2029 if not branches:
2030 print('No local branches found.')
2031 return 0
2032
2033 # Create a dictionary of all local branches to the branches that are dependent
2034 # on it.
2035 tracked_to_dependents = collections.defaultdict(list)
2036 for b in branches.splitlines():
2037 tokens = b.split()
2038 if len(tokens) == 2:
2039 branch_name, tracked = tokens
2040 tracked_to_dependents[tracked].append(branch_name)
2041
2042 print
2043 print 'The dependent local branches of %s are:' % root_branch
2044 dependents = []
2045 def traverse_dependents_preorder(branch, padding=''):
2046 dependents_to_process = tracked_to_dependents.get(branch, [])
2047 padding += ' '
2048 for dependent in dependents_to_process:
2049 print '%s%s' % (padding, dependent)
2050 dependents.append(dependent)
2051 traverse_dependents_preorder(dependent, padding)
2052 traverse_dependents_preorder(root_branch)
2053 print
2054
2055 if not dependents:
2056 print 'There are no dependent local branches for %s' % root_branch
2057 return 0
2058
2059 print ('This command will checkout all dependent branches and run '
2060 '"git cl upload".')
2061 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2062
andybons@chromium.org962f9462016-02-03 20:00:42 +00002063 # Add a default patchset title to all upload calls in Rietveld.
2064 if not settings.GetIsGerrit():
2065 args.extend(['-t', 'Updated patchset dependency'])
2066
rmistry@google.com2dd99862015-06-22 12:22:18 +00002067 # Record all dependents that failed to upload.
2068 failures = {}
2069 # Go through all dependents, checkout the branch and upload.
2070 try:
2071 for dependent_branch in dependents:
2072 print
2073 print '--------------------------------------'
2074 print 'Running "git cl upload" from %s:' % dependent_branch
2075 RunGit(['checkout', '-q', dependent_branch])
2076 print
2077 try:
2078 if CMDupload(OptionParser(), args) != 0:
2079 print 'Upload failed for %s!' % dependent_branch
2080 failures[dependent_branch] = 1
2081 except: # pylint: disable=W0702
2082 failures[dependent_branch] = 1
2083 print
2084 finally:
2085 # Swap back to the original root branch.
2086 RunGit(['checkout', '-q', root_branch])
2087
2088 print
2089 print 'Upload complete for dependent branches!'
2090 for dependent_branch in dependents:
2091 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2092 print ' %s : %s' % (dependent_branch, upload_status)
2093 print
2094
2095 return 0
2096
2097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002099 """Show status of changelists.
2100
2101 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002102 - Red not sent for review or broken
2103 - Blue waiting for review
2104 - Yellow waiting for you to reply to review
2105 - Green LGTM'ed
2106 - Magenta in the commit queue
2107 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002108
2109 Also see 'git cl comments'.
2110 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 parser.add_option('--field',
2112 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002113 parser.add_option('-f', '--fast', action='store_true',
2114 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002115 parser.add_option(
2116 '-j', '--maxjobs', action='store', type=int,
2117 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002118
2119 auth.add_auth_options(parser)
2120 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002121 if args:
2122 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002123 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002126 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 if options.field.startswith('desc'):
2128 print cl.GetDescription()
2129 elif options.field == 'id':
2130 issueid = cl.GetIssue()
2131 if issueid:
2132 print issueid
2133 elif options.field == 'patch':
2134 patchset = cl.GetPatchset()
2135 if patchset:
2136 print patchset
2137 elif options.field == 'url':
2138 url = cl.GetIssueURL()
2139 if url:
2140 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002141 return 0
2142
2143 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2144 if not branches:
2145 print('No local branch found.')
2146 return 0
2147
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002148 changes = (
2149 Changelist(branchref=b, auth_config=auth_config)
2150 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002151 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002152 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002153 alignment = max(5, max(len(b) for b in branches))
2154 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002155 output = get_cl_statuses(branches,
2156 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002157 max_processes=options.maxjobs,
2158 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002159
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002160 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002161 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002162 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002163 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002164 b, i, status = output.next()
2165 branch_statuses[b] = (i, status)
2166 issue_url, status = branch_statuses.pop(branch)
2167 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002168 reset = Fore.RESET
2169 if not sys.stdout.isatty():
2170 color = ''
2171 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002172 status_str = '(%s)' % status if status else ''
2173 print ' %*s : %s%s %s%s' % (
2174 alignment, ShortBranchName(branch), color, issue_url, status_str,
2175 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002176
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002177 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002178 print
2179 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002180 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002181 if not cl.GetIssue():
2182 print 'No issue assigned.'
2183 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002184 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002185 if not options.fast:
2186 print 'Issue description:'
2187 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002188 return 0
2189
2190
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002191def colorize_CMDstatus_doc():
2192 """To be called once in main() to add colors to git cl status help."""
2193 colors = [i for i in dir(Fore) if i[0].isupper()]
2194
2195 def colorize_line(line):
2196 for color in colors:
2197 if color in line.upper():
2198 # Extract whitespaces first and the leading '-'.
2199 indent = len(line) - len(line.lstrip(' ')) + 1
2200 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2201 return line
2202
2203 lines = CMDstatus.__doc__.splitlines()
2204 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2205
2206
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002207@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002208def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002209 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002210
2211 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002212 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002213 parser.add_option('-r', '--reverse', action='store_true',
2214 help='Lookup the branch(es) for the specified issues. If '
2215 'no issues are specified, all branches with mapped '
2216 'issues will be listed.')
2217 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002218
dnj@chromium.org406c4402015-03-03 17:22:28 +00002219 if options.reverse:
2220 branches = RunGit(['for-each-ref', 'refs/heads',
2221 '--format=%(refname:short)']).splitlines()
2222
2223 # Reverse issue lookup.
2224 issue_branch_map = {}
2225 for branch in branches:
2226 cl = Changelist(branchref=branch)
2227 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2228 if not args:
2229 args = sorted(issue_branch_map.iterkeys())
2230 for issue in args:
2231 if not issue:
2232 continue
2233 print 'Branch for issue number %s: %s' % (
2234 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2235 else:
2236 cl = Changelist()
2237 if len(args) > 0:
2238 try:
2239 issue = int(args[0])
2240 except ValueError:
2241 DieWithError('Pass a number to set the issue or none to list it.\n'
2242 'Maybe you want to run git cl status?')
2243 cl.SetIssue(issue)
2244 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002245 return 0
2246
2247
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002248def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002249 """Shows or posts review comments for any changelist."""
2250 parser.add_option('-a', '--add-comment', dest='comment',
2251 help='comment to add to an issue')
2252 parser.add_option('-i', dest='issue',
2253 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002254 parser.add_option('-j', '--json-file',
2255 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002256 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002257 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002258 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002259
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002260 issue = None
2261 if options.issue:
2262 try:
2263 issue = int(options.issue)
2264 except ValueError:
2265 DieWithError('A review issue id is expected to be a number')
2266
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002267 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002268
2269 if options.comment:
2270 cl.AddComment(options.comment)
2271 return 0
2272
2273 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002274 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002275 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002276 summary.append({
2277 'date': message['date'],
2278 'lgtm': False,
2279 'message': message['text'],
2280 'not_lgtm': False,
2281 'sender': message['sender'],
2282 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002283 if message['disapproval']:
2284 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002285 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002286 elif message['approval']:
2287 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002288 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002289 elif message['sender'] == data['owner_email']:
2290 color = Fore.MAGENTA
2291 else:
2292 color = Fore.BLUE
2293 print '\n%s%s %s%s' % (
2294 color, message['date'].split('.', 1)[0], message['sender'],
2295 Fore.RESET)
2296 if message['text'].strip():
2297 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002298 if options.json_file:
2299 with open(options.json_file, 'wb') as f:
2300 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002301 return 0
2302
2303
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002304def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002305 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002306 parser.add_option('-d', '--display', action='store_true',
2307 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002308 auth.add_auth_options(parser)
2309 options, _ = parser.parse_args(args)
2310 auth_config = auth.extract_auth_config_from_options(options)
2311 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002312 if not cl.GetIssue():
2313 DieWithError('This branch has no associated changelist.')
2314 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002315 if options.display:
2316 print description.description
2317 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002318 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002319 if cl.GetDescription() != description.description:
2320 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002321 return 0
2322
2323
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002324def CreateDescriptionFromLog(args):
2325 """Pulls out the commit log to use as a base for the CL description."""
2326 log_args = []
2327 if len(args) == 1 and not args[0].endswith('.'):
2328 log_args = [args[0] + '..']
2329 elif len(args) == 1 and args[0].endswith('...'):
2330 log_args = [args[0][:-1]]
2331 elif len(args) == 2:
2332 log_args = [args[0] + '..' + args[1]]
2333 else:
2334 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002335 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002336
2337
thestig@chromium.org44202a22014-03-11 19:22:18 +00002338def CMDlint(parser, args):
2339 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002340 parser.add_option('--filter', action='append', metavar='-x,+y',
2341 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002342 auth.add_auth_options(parser)
2343 options, args = parser.parse_args(args)
2344 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002345
2346 # Access to a protected member _XX of a client class
2347 # pylint: disable=W0212
2348 try:
2349 import cpplint
2350 import cpplint_chromium
2351 except ImportError:
2352 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2353 return 1
2354
2355 # Change the current working directory before calling lint so that it
2356 # shows the correct base.
2357 previous_cwd = os.getcwd()
2358 os.chdir(settings.GetRoot())
2359 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002360 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002361 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2362 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002363 if not files:
2364 print "Cannot lint an empty CL"
2365 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002366
2367 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002368 command = args + files
2369 if options.filter:
2370 command = ['--filter=' + ','.join(options.filter)] + command
2371 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002372
2373 white_regex = re.compile(settings.GetLintRegex())
2374 black_regex = re.compile(settings.GetLintIgnoreRegex())
2375 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2376 for filename in filenames:
2377 if white_regex.match(filename):
2378 if black_regex.match(filename):
2379 print "Ignoring file %s" % filename
2380 else:
2381 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2382 extra_check_functions)
2383 else:
2384 print "Skipping file %s" % filename
2385 finally:
2386 os.chdir(previous_cwd)
2387 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2388 if cpplint._cpplint_state.error_count != 0:
2389 return 1
2390 return 0
2391
2392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002393def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002394 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002395 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002396 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002397 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002398 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002399 auth.add_auth_options(parser)
2400 options, args = parser.parse_args(args)
2401 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002402
sbc@chromium.org71437c02015-04-09 19:29:40 +00002403 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002404 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002405 return 1
2406
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002407 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002408 if args:
2409 base_branch = args[0]
2410 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002411 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002412 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002413
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002414 cl.RunHook(
2415 committing=not options.upload,
2416 may_prompt=False,
2417 verbose=options.verbose,
2418 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002419 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002420
2421
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002422def AddChangeIdToCommitMessage(options, args):
2423 """Re-commits using the current message, assumes the commit hook is in
2424 place.
2425 """
2426 log_desc = options.message or CreateDescriptionFromLog(args)
2427 git_command = ['commit', '--amend', '-m', log_desc]
2428 RunGit(git_command)
2429 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002430 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002431 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002432 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002433 else:
2434 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2435
2436
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002437def GenerateGerritChangeId(message):
2438 """Returns Ixxxxxx...xxx change id.
2439
2440 Works the same way as
2441 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2442 but can be called on demand on all platforms.
2443
2444 The basic idea is to generate git hash of a state of the tree, original commit
2445 message, author/committer info and timestamps.
2446 """
2447 lines = []
2448 tree_hash = RunGitSilent(['write-tree'])
2449 lines.append('tree %s' % tree_hash.strip())
2450 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2451 if code == 0:
2452 lines.append('parent %s' % parent.strip())
2453 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2454 lines.append('author %s' % author.strip())
2455 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2456 lines.append('committer %s' % committer.strip())
2457 lines.append('')
2458 # Note: Gerrit's commit-hook actually cleans message of some lines and
2459 # whitespace. This code is not doing this, but it clearly won't decrease
2460 # entropy.
2461 lines.append(message)
2462 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2463 stdin='\n'.join(lines))
2464 return 'I%s' % change_hash.strip()
2465
2466
piman@chromium.org336f9122014-09-04 02:16:55 +00002467def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002468 """upload the current branch to gerrit."""
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002469 # We assume the remote called "origin" is the one we want.
2470 # It is probably not worthwhile to support different workflows.
2471 gerrit_remote = 'origin'
2472
luqui@chromium.org609f3952015-05-04 22:47:04 +00002473 remote, remote_branch = cl.GetRemoteBranch()
2474 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2475 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002476
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002477 change_desc = ChangeDescription(
2478 options.message or CreateDescriptionFromLog(args))
2479 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002480 print "\nDescription is empty. Aborting..."
2481 return 1
2482
2483 if options.title:
2484 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002485 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002486
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002487 if options.squash:
2488 # Try to get the message from a previous upload.
2489 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002490 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002491 if not message:
2492 if not options.force:
2493 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002494 if not change_desc.description:
2495 print "Description is empty; aborting."
2496 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002497 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002498 change_ids = git_footers.get_footer_change_id(message)
2499 if len(change_ids) > 1:
2500 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2501 if not change_ids:
2502 message = git_footers.add_footer_change_id(
2503 message, GenerateGerritChangeId(message))
2504 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002505 change_ids = git_footers.get_footer_change_id(message)
2506 assert len(change_ids) == 1
2507
2508 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002509
2510 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2511 if remote is '.':
2512 # If our upstream branch is local, we base our squashed commit on its
2513 # squashed version.
2514 parent = ('refs/heads/git_cl_uploads/' +
2515 scm.GIT.ShortBranchName(upstream_branch))
2516
2517 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2518 # will create additional CLs when uploading.
2519 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2520 RunGitSilent(['rev-parse', parent + ':'])):
2521 print 'Upload upstream branch ' + upstream_branch + ' first.'
2522 return 1
2523 else:
2524 parent = cl.GetCommonAncestorWithUpstream()
2525
2526 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2527 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2528 '-m', message]).strip()
2529 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002530 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002531 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002532 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002533 ref_to_push = 'HEAD'
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002534 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002535 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002536
2537 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2538 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002539 if len(commits) > 1:
2540 print('WARNING: This will upload %d commits. Run the following command '
2541 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002542 print('git log %s..%s' % (parent, ref_to_push))
2543 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002544 'commit.')
2545 ask_for_data('About to upload; enter to confirm.')
2546
piman@chromium.org336f9122014-09-04 02:16:55 +00002547 if options.reviewers or options.tbr_owners:
2548 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002549
ukai@chromium.orge8077812012-02-03 03:41:46 +00002550 receive_options = []
2551 cc = cl.GetCCList().split(',')
2552 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002553 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002554 cc = filter(None, cc)
2555 if cc:
2556 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002557 if change_desc.get_reviewers():
2558 receive_options.extend(
2559 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002560
ukai@chromium.orge8077812012-02-03 03:41:46 +00002561 git_command = ['push']
2562 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002563 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002564 ' '.join(receive_options))
tandrii@chromium.org95ffb612016-03-28 15:44:07 +00002565 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002566 push_stdout = gclient_utils.CheckCallAndFilter(
2567 ['git'] + git_command,
2568 print_stdout=True,
2569 # Flush after every line: useful for seeing progress when running as
2570 # recipe.
2571 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002572
2573 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002574 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2575 change_numbers = [m.group(1)
2576 for m in map(regex.match, push_stdout.splitlines())
2577 if m]
2578 if len(change_numbers) != 1:
2579 DieWithError(
2580 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2581 'Change-Id: %s') % (len(change_numbers), change_id))
2582 cl.SetIssue(change_numbers[0])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002583 head = RunGit(['rev-parse', 'HEAD']).strip()
2584 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002585 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002586
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002587
wittman@chromium.org455dc922015-01-26 20:15:50 +00002588def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2589 """Computes the remote branch ref to use for the CL.
2590
2591 Args:
2592 remote (str): The git remote for the CL.
2593 remote_branch (str): The git remote branch for the CL.
2594 target_branch (str): The target branch specified by the user.
2595 pending_prefix (str): The pending prefix from the settings.
2596 """
2597 if not (remote and remote_branch):
2598 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002599
wittman@chromium.org455dc922015-01-26 20:15:50 +00002600 if target_branch:
2601 # Cannonicalize branch references to the equivalent local full symbolic
2602 # refs, which are then translated into the remote full symbolic refs
2603 # below.
2604 if '/' not in target_branch:
2605 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2606 else:
2607 prefix_replacements = (
2608 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2609 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2610 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2611 )
2612 match = None
2613 for regex, replacement in prefix_replacements:
2614 match = re.search(regex, target_branch)
2615 if match:
2616 remote_branch = target_branch.replace(match.group(0), replacement)
2617 break
2618 if not match:
2619 # This is a branch path but not one we recognize; use as-is.
2620 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002621 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2622 # Handle the refs that need to land in different refs.
2623 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002624
wittman@chromium.org455dc922015-01-26 20:15:50 +00002625 # Create the true path to the remote branch.
2626 # Does the following translation:
2627 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2628 # * refs/remotes/origin/master -> refs/heads/master
2629 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2630 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2631 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2632 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2633 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2634 'refs/heads/')
2635 elif remote_branch.startswith('refs/remotes/branch-heads'):
2636 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2637 # If a pending prefix exists then replace refs/ with it.
2638 if pending_prefix:
2639 remote_branch = remote_branch.replace('refs/', pending_prefix)
2640 return remote_branch
2641
2642
piman@chromium.org336f9122014-09-04 02:16:55 +00002643def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002644 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002645 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002646 upload_args.extend(['--server', cl.GetCodereviewServer()])
2647 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
2648 upload_args.extend(auth.auth_config_to_command_options(
2649 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002650 if options.emulate_svn_auto_props:
2651 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002652
2653 change_desc = None
2654
pgervais@chromium.org91141372014-01-09 23:27:20 +00002655 if options.email is not None:
2656 upload_args.extend(['--email', options.email])
2657
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002658 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002659 if options.title:
2660 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002661 if options.message:
2662 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002663 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002664 print ("This branch is associated with issue %s. "
2665 "Adding patch to that issue." % cl.GetIssue())
2666 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002667 if options.title:
2668 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002669 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002670 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002671 if options.reviewers or options.tbr_owners:
2672 change_desc.update_reviewers(options.reviewers,
2673 options.tbr_owners,
2674 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002675 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002676 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002677
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002678 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002679 print "Description is empty; aborting."
2680 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002681
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002682 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002683 if change_desc.get_reviewers():
2684 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002685 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002686 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002687 DieWithError("Must specify reviewers to send email.")
2688 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002689
2690 # We check this before applying rietveld.private assuming that in
2691 # rietveld.cc only addresses which we can send private CLs to are listed
2692 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2693 # --private is specified explicitly on the command line.
2694 if options.private:
2695 logging.warn('rietveld.cc is ignored since private flag is specified. '
2696 'You need to review and add them manually if necessary.')
2697 cc = cl.GetCCListWithoutDefault()
2698 else:
2699 cc = cl.GetCCList()
2700 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002701 if cc:
2702 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002703
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002704 if options.private or settings.GetDefaultPrivateFlag() == "True":
2705 upload_args.append('--private')
2706
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002707 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002708 if not options.find_copies:
2709 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002711 # Include the upstream repo's URL in the change -- this is useful for
2712 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002713 remote_url = cl.GetGitBaseUrlFromConfig()
2714 if not remote_url:
2715 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002716 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002717 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002718 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2719 remote_url = (cl.GetRemoteUrl() + '@'
2720 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002721 if remote_url:
2722 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002723 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002724 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2725 settings.GetPendingRefPrefix())
2726 if target_ref:
2727 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002728
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002729 # Look for dependent patchsets. See crbug.com/480453 for more details.
2730 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2731 upstream_branch = ShortBranchName(upstream_branch)
2732 if remote is '.':
2733 # A local branch is being tracked.
2734 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002735 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002736 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002737 print ('Skipping dependency patchset upload because git config '
2738 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002739 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002740 else:
2741 auth_config = auth.extract_auth_config_from_options(options)
2742 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2743 branch_cl_issue_url = branch_cl.GetIssueURL()
2744 branch_cl_issue = branch_cl.GetIssue()
2745 branch_cl_patchset = branch_cl.GetPatchset()
2746 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2747 upload_args.extend(
2748 ['--depends_on_patchset', '%s:%s' % (
2749 branch_cl_issue, branch_cl_patchset)])
2750 print
2751 print ('The current branch (%s) is tracking a local branch (%s) with '
2752 'an associated CL.') % (cl.GetBranch(), local_branch)
2753 print 'Adding %s/#ps%s as a dependency patchset.' % (
2754 branch_cl_issue_url, branch_cl_patchset)
2755 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002756
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002757 project = settings.GetProject()
2758 if project:
2759 upload_args.extend(['--project', project])
2760
rmistry@google.comef966222015-04-07 11:15:01 +00002761 if options.cq_dry_run:
2762 upload_args.extend(['--cq_dry_run'])
2763
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002764 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002765 upload_args = ['upload'] + upload_args + args
2766 logging.info('upload.RealMain(%s)', upload_args)
2767 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002768 issue = int(issue)
2769 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002770 except KeyboardInterrupt:
2771 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002772 except:
2773 # If we got an exception after the user typed a description for their
2774 # change, back up the description before re-raising.
2775 if change_desc:
2776 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2777 print '\nGot exception while uploading -- saving description to %s\n' \
2778 % backup_path
2779 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002780 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002781 backup_file.close()
2782 raise
2783
2784 if not cl.GetIssue():
2785 cl.SetIssue(issue)
2786 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002787
2788 if options.use_commit_queue:
2789 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002790 return 0
2791
2792
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002793def cleanup_list(l):
2794 """Fixes a list so that comma separated items are put as individual items.
2795
2796 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2797 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2798 """
2799 items = sum((i.split(',') for i in l), [])
2800 stripped_items = (i.strip() for i in items)
2801 return sorted(filter(None, stripped_items))
2802
2803
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002804@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002805def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002806 """Uploads the current changelist to codereview.
2807
2808 Can skip dependency patchset uploads for a branch by running:
2809 git config branch.branch_name.skip-deps-uploads True
2810 To unset run:
2811 git config --unset branch.branch_name.skip-deps-uploads
2812 Can also set the above globally by using the --global flag.
2813 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002814 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2815 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002816 parser.add_option('--bypass-watchlists', action='store_true',
2817 dest='bypass_watchlists',
2818 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002819 parser.add_option('-f', action='store_true', dest='force',
2820 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002821 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002822 parser.add_option('-t', dest='title',
2823 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002824 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002825 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002826 help='reviewer email addresses')
2827 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002828 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002829 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002830 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002831 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002832 parser.add_option('--emulate_svn_auto_props',
2833 '--emulate-svn-auto-props',
2834 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002835 dest="emulate_svn_auto_props",
2836 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002837 parser.add_option('-c', '--use-commit-queue', action='store_true',
2838 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002839 parser.add_option('--private', action='store_true',
2840 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002841 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002842 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002843 metavar='TARGET',
2844 help='Apply CL to remote ref TARGET. ' +
2845 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002846 parser.add_option('--squash', action='store_true',
2847 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002848 parser.add_option('--no-squash', action='store_true',
2849 help='Don\'t squash multiple commits into one ' +
2850 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002851 parser.add_option('--email', default=None,
2852 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002853 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2854 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002855 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2856 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002857 help='Send the patchset to do a CQ dry run right after '
2858 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002859 parser.add_option('--dependencies', action='store_true',
2860 help='Uploads CLs of all the local branches that depend on '
2861 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002862
rmistry@google.com2dd99862015-06-22 12:22:18 +00002863 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002864 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002865 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002866 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002867 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002868
sbc@chromium.org71437c02015-04-09 19:29:40 +00002869 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002870 return 1
2871
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002872 options.reviewers = cleanup_list(options.reviewers)
2873 options.cc = cleanup_list(options.cc)
2874
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002875 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002876 if args:
2877 # TODO(ukai): is it ok for gerrit case?
2878 base_branch = args[0]
2879 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002880 if cl.GetBranch() is None:
2881 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2882
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002883 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002884 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002885 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002886
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002887 # Make sure authenticated to Rietveld before running expensive hooks. It is
2888 # a fast, best efforts check. Rietveld still can reject the authentication
2889 # during the actual upload.
2890 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2891 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002892 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002893 if not authenticator.has_cached_credentials():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002894 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002895
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002896 # Apply watchlists on upload.
2897 change = cl.GetChange(base_branch, None)
2898 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2899 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002900 if not options.bypass_watchlists:
2901 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002902
ukai@chromium.orge8077812012-02-03 03:41:46 +00002903 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002904 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002905 # Set the reviewer list now so that presubmit checks can access it.
2906 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002907 change_description.update_reviewers(options.reviewers,
2908 options.tbr_owners,
2909 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002910 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002911 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002912 may_prompt=not options.force,
2913 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002914 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002915 if not hook_results.should_continue():
2916 return 1
2917 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002918 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002919
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002920 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002921 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002922 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002923 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002924 print ('The last upload made from this repository was patchset #%d but '
2925 'the most recent patchset on the server is #%d.'
2926 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002927 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2928 'from another machine or branch the patch you\'re uploading now '
2929 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002930 ask_for_data('About to upload; enter to confirm.')
2931
iannucci@chromium.org79540052012-10-19 23:15:26 +00002932 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002933 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002934 if options.squash and options.no_squash:
2935 DieWithError('Can only use one of --squash or --no-squash')
2936
2937 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2938 not options.no_squash)
2939
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002940 ret = GerritUpload(options, args, cl, change)
2941 else:
2942 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002943 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002944 git_set_branch_value('last-upload-hash',
2945 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002946 # Run post upload hooks, if specified.
2947 if settings.GetRunPostUploadHook():
2948 presubmit_support.DoPostUploadExecuter(
2949 change,
2950 cl,
2951 settings.GetRoot(),
2952 options.verbose,
2953 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002954
rmistry@google.com2dd99862015-06-22 12:22:18 +00002955 # Upload all dependencies if specified.
2956 if options.dependencies:
2957 print
2958 print '--dependencies has been specified.'
2959 print 'All dependent local branches will be re-uploaded.'
2960 print
2961 # Remove the dependencies flag from args so that we do not end up in a
2962 # loop.
2963 orig_args.remove('--dependencies')
2964 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002965 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002966
2967
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002968def IsSubmoduleMergeCommit(ref):
2969 # When submodules are added to the repo, we expect there to be a single
2970 # non-git-svn merge commit at remote HEAD with a signature comment.
2971 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002972 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002973 return RunGit(cmd) != ''
2974
2975
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002976def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002977 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002978
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002979 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002980 Updates changelog with metadata (e.g. pointer to review).
2981 Pushes/dcommits the code upstream.
2982 Updates review and closes.
2983 """
2984 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2985 help='bypass upload presubmit hook')
2986 parser.add_option('-m', dest='message',
2987 help="override review description")
2988 parser.add_option('-f', action='store_true', dest='force',
2989 help="force yes to questions (don't prompt)")
2990 parser.add_option('-c', dest='contributor',
2991 help="external contributor for patch (appended to " +
2992 "description and used as author for git). Should be " +
2993 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002994 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002995 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002996 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002997 auth_config = auth.extract_auth_config_from_options(options)
2998
2999 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003000
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003001 current = cl.GetBranch()
3002 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3003 if not settings.GetIsGitSvn() and remote == '.':
3004 print
3005 print 'Attempting to push branch %r into another local branch!' % current
3006 print
3007 print 'Either reparent this branch on top of origin/master:'
3008 print ' git reparent-branch --root'
3009 print
3010 print 'OR run `git rebase-update` if you think the parent branch is already'
3011 print 'committed.'
3012 print
3013 print ' Current parent: %r' % upstream_branch
3014 return 1
3015
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003016 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003017 # Default to merging against our best guess of the upstream branch.
3018 args = [cl.GetUpstreamBranch()]
3019
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003020 if options.contributor:
3021 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3022 print "Please provide contibutor as 'First Last <email@example.com>'"
3023 return 1
3024
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003025 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003026 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003027
sbc@chromium.org71437c02015-04-09 19:29:40 +00003028 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003029 return 1
3030
3031 # This rev-list syntax means "show all commits not in my branch that
3032 # are in base_branch".
3033 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3034 base_branch]).splitlines()
3035 if upstream_commits:
3036 print ('Base branch "%s" has %d commits '
3037 'not in this branch.' % (base_branch, len(upstream_commits)))
3038 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3039 return 1
3040
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003041 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003042 svn_head = None
3043 if cmd == 'dcommit' or base_has_submodules:
3044 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3045 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003046
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003047 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003048 # If the base_head is a submodule merge commit, the first parent of the
3049 # base_head should be a git-svn commit, which is what we're interested in.
3050 base_svn_head = base_branch
3051 if base_has_submodules:
3052 base_svn_head += '^1'
3053
3054 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003055 if extra_commits:
3056 print ('This branch has %d additional commits not upstreamed yet.'
3057 % len(extra_commits.splitlines()))
3058 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3059 'before attempting to %s.' % (base_branch, cmd))
3060 return 1
3061
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003062 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003063 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003064 author = None
3065 if options.contributor:
3066 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003067 hook_results = cl.RunHook(
3068 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003069 may_prompt=not options.force,
3070 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003071 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003072 if not hook_results.should_continue():
3073 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003074
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003075 # Check the tree status if the tree status URL is set.
3076 status = GetTreeStatus()
3077 if 'closed' == status:
3078 print('The tree is closed. Please wait for it to reopen. Use '
3079 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3080 return 1
3081 elif 'unknown' == status:
3082 print('Unable to determine tree status. Please verify manually and '
3083 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3084 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003085
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003086 change_desc = ChangeDescription(options.message)
3087 if not change_desc.description and cl.GetIssue():
3088 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003089
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003090 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003091 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003092 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003093 else:
3094 print 'No description set.'
3095 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3096 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003098 # Keep a separate copy for the commit message, because the commit message
3099 # contains the link to the Rietveld issue, while the Rietveld message contains
3100 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003101 # Keep a separate copy for the commit message.
3102 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003103 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003104
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003105 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003106 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003107 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003108 # after it. Add a period on a new line to circumvent this. Also add a space
3109 # before the period to make sure that Gitiles continues to correctly resolve
3110 # the URL.
3111 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003112 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003113 commit_desc.append_footer('Patch from %s.' % options.contributor)
3114
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003115 print('Description:')
3116 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003117
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003118 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003119 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003120 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003121
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003122 # We want to squash all this branch's commits into one commit with the proper
3123 # description. We do this by doing a "reset --soft" to the base branch (which
3124 # keeps the working copy the same), then dcommitting that. If origin/master
3125 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3126 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003127 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003128 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3129 # Delete the branches if they exist.
3130 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3131 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3132 result = RunGitWithCode(showref_cmd)
3133 if result[0] == 0:
3134 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003135
3136 # We might be in a directory that's present in this branch but not in the
3137 # trunk. Move up to the top of the tree so that git commands that expect a
3138 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003139 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003140 if rel_base_path:
3141 os.chdir(rel_base_path)
3142
3143 # Stuff our change into the merge branch.
3144 # We wrap in a try...finally block so if anything goes wrong,
3145 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003146 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003147 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003148 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003149 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003150 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003151 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003152 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003153 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003154 RunGit(
3155 [
3156 'commit', '--author', options.contributor,
3157 '-m', commit_desc.description,
3158 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003159 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003160 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003161 if base_has_submodules:
3162 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3163 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3164 RunGit(['checkout', CHERRY_PICK_BRANCH])
3165 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003166 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003167 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003168 mirror = settings.GetGitMirror(remote)
3169 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003170 pending_prefix = settings.GetPendingRefPrefix()
3171 if not pending_prefix or branch.startswith(pending_prefix):
3172 # If not using refs/pending/heads/* at all, or target ref is already set
3173 # to pending, then push to the target ref directly.
3174 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003175 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003176 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003177 else:
3178 # Cherry-pick the change on top of pending ref and then push it.
3179 assert branch.startswith('refs/'), branch
3180 assert pending_prefix[-1] == '/', pending_prefix
3181 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003182 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003183 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003184 if retcode == 0:
3185 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003186 else:
3187 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003188 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003189 'svn', 'dcommit',
3190 '-C%s' % options.similarity,
3191 '--no-rebase', '--rmdir',
3192 ]
3193 if settings.GetForceHttpsCommitUrl():
3194 # Allow forcing https commit URLs for some projects that don't allow
3195 # committing to http URLs (like Google Code).
3196 remote_url = cl.GetGitSvnRemoteUrl()
3197 if urlparse.urlparse(remote_url).scheme == 'http':
3198 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003199 cmd_args.append('--commit-url=%s' % remote_url)
3200 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003201 if 'Committed r' in output:
3202 revision = re.match(
3203 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3204 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 finally:
3206 # And then swap back to the original branch and clean up.
3207 RunGit(['checkout', '-q', cl.GetBranch()])
3208 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003209 if base_has_submodules:
3210 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003211
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003212 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003213 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003214 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003215
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003216 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003217 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003218 try:
3219 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3220 # We set pushed_to_pending to False, since it made it all the way to the
3221 # real ref.
3222 pushed_to_pending = False
3223 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003224 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003227 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003229 if not to_pending:
3230 if viewvc_url and revision:
3231 change_desc.append_footer(
3232 'Committed: %s%s' % (viewvc_url, revision))
3233 elif revision:
3234 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003235 print ('Closing issue '
3236 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003237 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003238 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003239 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003240 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003241 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003242 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003243 if options.bypass_hooks:
3244 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3245 else:
3246 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003247 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003248 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003249
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003250 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003251 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3252 print 'The commit is in the pending queue (%s).' % pending_ref
3253 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003254 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003255 'footer.' % branch)
3256
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003257 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3258 if os.path.isfile(hook):
3259 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003260
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003261 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262
3263
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003264def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3265 print
3266 print 'Waiting for commit to be landed on %s...' % real_ref
3267 print '(If you are impatient, you may Ctrl-C once without harm)'
3268 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3269 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003270 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003271
3272 loop = 0
3273 while True:
3274 sys.stdout.write('fetching (%d)... \r' % loop)
3275 sys.stdout.flush()
3276 loop += 1
3277
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003278 if mirror:
3279 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003280 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3281 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3282 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3283 for commit in commits.splitlines():
3284 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3285 print 'Found commit on %s' % real_ref
3286 return commit
3287
3288 current_rev = to_rev
3289
3290
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003291def PushToGitPending(remote, pending_ref, upstream_ref):
3292 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3293
3294 Returns:
3295 (retcode of last operation, output log of last operation).
3296 """
3297 assert pending_ref.startswith('refs/'), pending_ref
3298 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3299 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3300 code = 0
3301 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003302 max_attempts = 3
3303 attempts_left = max_attempts
3304 while attempts_left:
3305 if attempts_left != max_attempts:
3306 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3307 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003308
3309 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003310 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003311 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003312 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003313 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003314 print 'Fetch failed with exit code %d.' % code
3315 if out.strip():
3316 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003317 continue
3318
3319 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003320 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003321 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003322 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003323 if code:
3324 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003325 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3326 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003327 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3328 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003329 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003330 return code, out
3331
3332 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003333 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003334 code, out = RunGitWithCode(
3335 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3336 if code == 0:
3337 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003338 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003339 return code, out
3340
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003341 print 'Push failed with exit code %d.' % code
3342 if out.strip():
3343 print out.strip()
3344 if IsFatalPushFailure(out):
3345 print (
3346 'Fatal push error. Make sure your .netrc credentials and git '
3347 'user.email are correct and you have push access to the repo.')
3348 return code, out
3349
3350 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003351 return code, out
3352
3353
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003354def IsFatalPushFailure(push_stdout):
3355 """True if retrying push won't help."""
3356 return '(prohibited by Gerrit)' in push_stdout
3357
3358
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003359@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003360def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003361 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003362 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003363 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003364 # If it looks like previous commits were mirrored with git-svn.
3365 message = """This repository appears to be a git-svn mirror, but no
3366upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3367 else:
3368 message = """This doesn't appear to be an SVN repository.
3369If your project has a true, writeable git repository, you probably want to run
3370'git cl land' instead.
3371If your project has a git mirror of an upstream SVN master, you probably need
3372to run 'git svn init'.
3373
3374Using the wrong command might cause your commit to appear to succeed, and the
3375review to be closed, without actually landing upstream. If you choose to
3376proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003377 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003378 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379 return SendUpstream(parser, args, 'dcommit')
3380
3381
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003382@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003383def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003384 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003385 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003386 print('This appears to be an SVN repository.')
3387 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003388 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003389 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003390 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003391
3392
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003393def ParseIssueNum(arg):
3394 """Parses the issue number from args if present otherwise returns None."""
3395 if re.match(r'\d+', arg):
3396 return arg
3397 if arg.startswith('http'):
3398 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3399 return None
3400
3401
3402@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003403def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003404 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003405 parser.add_option('-b', dest='newbranch',
3406 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003407 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003409 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3410 help='Change to the directory DIR immediately, '
3411 'before doing anything else.')
3412 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003413 help='failed patches spew .rej files rather than '
3414 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3416 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003417
3418 group = optparse.OptionGroup(parser,
3419 """Options for continuing work on the current issue uploaded
3420from a different clone (e.g. different machine). Must be used independently from
3421the other options. No issue number should be specified, and the branch must have
3422an issue number associated with it""")
3423 group.add_option('--reapply', action='store_true',
3424 dest='reapply',
3425 help="""Reset the branch and reapply the issue.
3426CAUTION: This will undo any local changes in this branch""")
3427
3428 group.add_option('--pull', action='store_true', dest='pull',
3429 help="Performs a pull before reapplying.")
3430 parser.add_option_group(group)
3431
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003432 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003433 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003434 auth_config = auth.extract_auth_config_from_options(options)
3435
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003436 issue_arg = None
3437 if options.reapply :
3438 if len(args) > 0:
3439 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003440
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003441 cl = Changelist()
3442 issue_arg = cl.GetIssue()
3443 upstream = cl.GetUpstreamBranch()
3444 if upstream == None:
3445 parser.error("No upstream branch specified. Cannot reset branch")
3446
3447 RunGit(['reset', '--hard', upstream])
3448 if options.pull:
3449 RunGit(['pull'])
3450 else:
3451 if len(args) != 1:
3452 parser.error("Must specify issue number")
3453
3454 issue_arg = ParseIssueNum(args[0])
3455
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003456 # The patch URL works because ParseIssueNum won't do any substitution
3457 # as the re.sub pattern fails to match and just returns it.
3458 if issue_arg == None:
3459 parser.print_help()
3460 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003461
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003462 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003463 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003464 return 1
3465
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003466 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003467 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003468
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003469 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003470 if options.reapply:
3471 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003472 if options.force:
3473 RunGit(['branch', '-D', options.newbranch],
3474 stderr=subprocess2.PIPE, error_ok=True)
3475 RunGit(['checkout', '-b', options.newbranch,
3476 Changelist().GetUpstreamBranch()])
3477
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003478 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003479 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003480
3481
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003482def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003483 # PatchIssue should never be called with a dirty tree. It is up to the
3484 # caller to check this, but just in case we assert here since the
3485 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003486 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003487
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003488 # TODO(tandrii): implement for Gerrit.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003489 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003490 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003491 issue = int(issue_arg)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003492 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003493 patchset = cl.GetMostRecentPatchset()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003494 patch_data = cl._codereview_impl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003495 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003496 # Assume it's a URL to the patch. Default to https.
3497 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003498 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003499 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003500 DieWithError('Must pass an issue ID or full URL for '
3501 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003502 issue = int(match.group(2))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003503 cl = Changelist(issue=issue, codereview='rietveld',
3504 rietvled_server=match.group(1), auth_config=auth_config)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003505 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003506 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003507
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003508 # Switch up to the top-level directory, if necessary, in preparation for
3509 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003510 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003511 if top:
3512 os.chdir(top)
3513
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003514 # Git patches have a/ at the beginning of source paths. We strip that out
3515 # with a sed script rather than the -p flag to patch so we can feed either
3516 # Git or svn-style patches into the same apply command.
3517 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003518 try:
3519 patch_data = subprocess2.check_output(
3520 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3521 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003522 DieWithError('Git patch mungling failed.')
3523 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003524
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525 # We use "git apply" to apply the patch instead of "patch" so that we can
3526 # pick up file adds.
3527 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003528 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003529 if directory:
3530 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003531 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003533 elif IsGitVersionAtLeast('1.7.12'):
3534 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003535 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003536 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003537 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003538 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003539 print 'Failed to apply the patch'
3540 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003541
3542 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003543 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003544 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3545 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003546 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3547 % {'i': issue, 'p': patchset})])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003548 cl = Changelist(codereview='rietveld', auth_config=auth_config,
3549 rietveld_server=cl.GetCodereviewServer())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003550 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003551 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003552 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003553 else:
3554 print "Patch applied to index."
3555 return 0
3556
3557
3558def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003559 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003560 # Provide a wrapper for git svn rebase to help avoid accidental
3561 # git svn dcommit.
3562 # It's the only command that doesn't use parser at all since we just defer
3563 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003564
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003565 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003566
3567
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003568def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003569 """Fetches the tree status and returns either 'open', 'closed',
3570 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003571 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003572 if url:
3573 status = urllib2.urlopen(url).read().lower()
3574 if status.find('closed') != -1 or status == '0':
3575 return 'closed'
3576 elif status.find('open') != -1 or status == '1':
3577 return 'open'
3578 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003579 return 'unset'
3580
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003582def GetTreeStatusReason():
3583 """Fetches the tree status from a json url and returns the message
3584 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003585 url = settings.GetTreeStatusUrl()
3586 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587 connection = urllib2.urlopen(json_url)
3588 status = json.loads(connection.read())
3589 connection.close()
3590 return status['message']
3591
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003592
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003593def GetBuilderMaster(bot_list):
3594 """For a given builder, fetch the master from AE if available."""
3595 map_url = 'https://builders-map.appspot.com/'
3596 try:
3597 master_map = json.load(urllib2.urlopen(map_url))
3598 except urllib2.URLError as e:
3599 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3600 (map_url, e))
3601 except ValueError as e:
3602 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3603 if not master_map:
3604 return None, 'Failed to build master map.'
3605
3606 result_master = ''
3607 for bot in bot_list:
3608 builder = bot.split(':', 1)[0]
3609 master_list = master_map.get(builder, [])
3610 if not master_list:
3611 return None, ('No matching master for builder %s.' % builder)
3612 elif len(master_list) > 1:
3613 return None, ('The builder name %s exists in multiple masters %s.' %
3614 (builder, master_list))
3615 else:
3616 cur_master = master_list[0]
3617 if not result_master:
3618 result_master = cur_master
3619 elif result_master != cur_master:
3620 return None, 'The builders do not belong to the same master.'
3621 return result_master, None
3622
3623
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003625 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003626 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003627 status = GetTreeStatus()
3628 if 'unset' == status:
3629 print 'You must configure your tree status URL by running "git cl config".'
3630 return 2
3631
3632 print "The tree is %s" % status
3633 print
3634 print GetTreeStatusReason()
3635 if status != 'open':
3636 return 1
3637 return 0
3638
3639
maruel@chromium.org15192402012-09-06 12:38:29 +00003640def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003641 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003642 group = optparse.OptionGroup(parser, "Try job options")
3643 group.add_option(
3644 "-b", "--bot", action="append",
3645 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3646 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003647 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003648 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003649 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003650 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003651 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003652 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003653 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003654 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003655 "-r", "--revision",
3656 help="Revision to use for the try job; default: the "
3657 "revision will be determined by the try server; see "
3658 "its waterfall for more info")
3659 group.add_option(
3660 "-c", "--clobber", action="store_true", default=False,
3661 help="Force a clobber before building; e.g. don't do an "
3662 "incremental build")
3663 group.add_option(
3664 "--project",
3665 help="Override which project to use. Projects are defined "
3666 "server-side to define what default bot set to use")
3667 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003668 "-p", "--property", dest="properties", action="append", default=[],
3669 help="Specify generic properties in the form -p key1=value1 -p "
3670 "key2=value2 etc (buildbucket only). The value will be treated as "
3671 "json if decodable, or as string otherwise.")
3672 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003673 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003674 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003675 "--use-rietveld", action="store_true", default=False,
3676 help="Use Rietveld to trigger try jobs.")
3677 group.add_option(
3678 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3679 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003680 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003681 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003682 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003683 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003684
machenbach@chromium.org45453142015-09-15 08:45:22 +00003685 if options.use_rietveld and options.properties:
3686 parser.error('Properties can only be specified with buildbucket')
3687
3688 # Make sure that all properties are prop=value pairs.
3689 bad_params = [x for x in options.properties if '=' not in x]
3690 if bad_params:
3691 parser.error('Got properties with missing "=": %s' % bad_params)
3692
maruel@chromium.org15192402012-09-06 12:38:29 +00003693 if args:
3694 parser.error('Unknown arguments: %s' % args)
3695
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003696 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003697 if not cl.GetIssue():
3698 parser.error('Need to upload first')
3699
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003700 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003701 if props.get('closed'):
3702 parser.error('Cannot send tryjobs for a closed CL')
3703
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003704 if props.get('private'):
3705 parser.error('Cannot use trybots with private issue')
3706
maruel@chromium.org15192402012-09-06 12:38:29 +00003707 if not options.name:
3708 options.name = cl.GetBranch()
3709
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003710 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003711 options.master, err_msg = GetBuilderMaster(options.bot)
3712 if err_msg:
3713 parser.error('Tryserver master cannot be found because: %s\n'
3714 'Please manually specify the tryserver master'
3715 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003716
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003717 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003718 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003719 if not options.bot:
3720 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003721
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003722 # Get try masters from PRESUBMIT.py files.
3723 masters = presubmit_support.DoGetTryMasters(
3724 change,
3725 change.LocalPaths(),
3726 settings.GetRoot(),
3727 None,
3728 None,
3729 options.verbose,
3730 sys.stdout)
3731 if masters:
3732 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003733
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003734 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3735 options.bot = presubmit_support.DoGetTrySlaves(
3736 change,
3737 change.LocalPaths(),
3738 settings.GetRoot(),
3739 None,
3740 None,
3741 options.verbose,
3742 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003743
3744 if not options.bot:
3745 # Get try masters from cq.cfg if any.
3746 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3747 # location.
3748 cq_cfg = os.path.join(change.RepositoryRoot(),
3749 'infra', 'config', 'cq.cfg')
3750 if os.path.exists(cq_cfg):
3751 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003752 cq_masters = commit_queue.get_master_builder_map(
3753 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003754 for master, builders in cq_masters.iteritems():
3755 for builder in builders:
3756 # Skip presubmit builders, because these will fail without LGTM.
3757 if 'presubmit' not in builder.lower():
3758 masters.setdefault(master, {})[builder] = ['defaulttests']
3759 if masters:
3760 return masters
3761
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003762 if not options.bot:
3763 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003764
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003765 builders_and_tests = {}
3766 # TODO(machenbach): The old style command-line options don't support
3767 # multiple try masters yet.
3768 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3769 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3770
3771 for bot in old_style:
3772 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003773 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003774 elif ',' in bot:
3775 parser.error('Specify one bot per --bot flag')
3776 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003777 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003778
3779 for bot, tests in new_style:
3780 builders_and_tests.setdefault(bot, []).extend(tests)
3781
3782 # Return a master map with one master to be backwards compatible. The
3783 # master name defaults to an empty string, which will cause the master
3784 # not to be set on rietveld (deprecated).
3785 return {options.master: builders_and_tests}
3786
3787 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003788
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003789 for builders in masters.itervalues():
3790 if any('triggered' in b for b in builders):
3791 print >> sys.stderr, (
3792 'ERROR You are trying to send a job to a triggered bot. This type of'
3793 ' bot requires an\ninitial job from a parent (usually a builder). '
3794 'Instead send your job to the parent.\n'
3795 'Bot list: %s' % builders)
3796 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003797
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003798 patchset = cl.GetMostRecentPatchset()
3799 if patchset and patchset != cl.GetPatchset():
3800 print(
3801 '\nWARNING Mismatch between local config and server. Did a previous '
3802 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3803 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003804 if options.luci:
3805 trigger_luci_job(cl, masters, options)
3806 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003807 try:
3808 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3809 except BuildbucketResponseException as ex:
3810 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003811 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003812 except Exception as e:
3813 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3814 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3815 e, stacktrace)
3816 return 1
3817 else:
3818 try:
3819 cl.RpcServer().trigger_distributed_try_jobs(
3820 cl.GetIssue(), patchset, options.name, options.clobber,
3821 options.revision, masters)
3822 except urllib2.HTTPError as e:
3823 if e.code == 404:
3824 print('404 from rietveld; '
3825 'did you mean to use "git try" instead of "git cl try"?')
3826 return 1
3827 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003828
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003829 for (master, builders) in sorted(masters.iteritems()):
3830 if master:
3831 print 'Master: %s' % master
3832 length = max(len(builder) for builder in builders)
3833 for builder in sorted(builders):
3834 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003835 return 0
3836
3837
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003838def CMDtry_results(parser, args):
3839 group = optparse.OptionGroup(parser, "Try job results options")
3840 group.add_option(
3841 "-p", "--patchset", type=int, help="patchset number if not current.")
3842 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003843 "--print-master", action='store_true', help="print master name as well.")
3844 group.add_option(
3845 "--color", action='store_true', default=sys.stdout.isatty(),
3846 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003847 group.add_option(
3848 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3849 help="Host of buildbucket. The default host is %default.")
3850 parser.add_option_group(group)
3851 auth.add_auth_options(parser)
3852 options, args = parser.parse_args(args)
3853 if args:
3854 parser.error('Unrecognized args: %s' % ' '.join(args))
3855
3856 auth_config = auth.extract_auth_config_from_options(options)
3857 cl = Changelist(auth_config=auth_config)
3858 if not cl.GetIssue():
3859 parser.error('Need to upload first')
3860
3861 if not options.patchset:
3862 options.patchset = cl.GetMostRecentPatchset()
3863 if options.patchset and options.patchset != cl.GetPatchset():
3864 print(
3865 '\nWARNING Mismatch between local config and server. Did a previous '
3866 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3867 'Continuing using\npatchset %s.\n' % options.patchset)
3868 try:
3869 jobs = fetch_try_jobs(auth_config, cl, options)
3870 except BuildbucketResponseException as ex:
3871 print 'Buildbucket error: %s' % ex
3872 return 1
3873 except Exception as e:
3874 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3875 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3876 e, stacktrace)
3877 return 1
3878 print_tryjobs(options, jobs)
3879 return 0
3880
3881
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003882@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003883def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003884 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003885 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003886 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003887 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003888
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003889 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003890 if args:
3891 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003892 branch = cl.GetBranch()
3893 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003894 cl = Changelist()
3895 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003896
3897 # Clear configured merge-base, if there is one.
3898 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003899 else:
3900 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003901 return 0
3902
3903
thestig@chromium.org00858c82013-12-02 23:08:03 +00003904def CMDweb(parser, args):
3905 """Opens the current CL in the web browser."""
3906 _, args = parser.parse_args(args)
3907 if args:
3908 parser.error('Unrecognized args: %s' % ' '.join(args))
3909
3910 issue_url = Changelist().GetIssueURL()
3911 if not issue_url:
3912 print >> sys.stderr, 'ERROR No issue to open'
3913 return 1
3914
3915 webbrowser.open(issue_url)
3916 return 0
3917
3918
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003919def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003920 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003921 auth.add_auth_options(parser)
3922 options, args = parser.parse_args(args)
3923 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003924 if args:
3925 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003926 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003927 props = cl.GetIssueProperties()
3928 if props.get('private'):
3929 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003930 cl.SetFlag('commit', '1')
3931 return 0
3932
3933
groby@chromium.org411034a2013-02-26 15:12:01 +00003934def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003935 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003936 auth.add_auth_options(parser)
3937 options, args = parser.parse_args(args)
3938 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003939 if args:
3940 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003941 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003942 # Ensure there actually is an issue to close.
3943 cl.GetDescription()
3944 cl.CloseIssue()
3945 return 0
3946
3947
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003948def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003949 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003950 auth.add_auth_options(parser)
3951 options, args = parser.parse_args(args)
3952 auth_config = auth.extract_auth_config_from_options(options)
3953 if args:
3954 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003955
3956 # Uncommitted (staged and unstaged) changes will be destroyed by
3957 # "git reset --hard" if there are merging conflicts in PatchIssue().
3958 # Staged changes would be committed along with the patch from last
3959 # upload, hence counted toward the "last upload" side in the final
3960 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003961 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003962 return 1
3963
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003964 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003965 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003966 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003967 if not issue:
3968 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003969 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003970 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003971
3972 # Create a new branch based on the merge-base
3973 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3974 try:
3975 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003976 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003977 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003978 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003979 return rtn
3980
wychen@chromium.org06928532015-02-03 02:11:29 +00003981 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003982 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003983 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003984 finally:
3985 RunGit(['checkout', '-q', branch])
3986 RunGit(['branch', '-D', TMP_BRANCH])
3987
3988 return 0
3989
3990
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003991def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003992 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003993 parser.add_option(
3994 '--no-color',
3995 action='store_true',
3996 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003997 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003998 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003999 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004000
4001 author = RunGit(['config', 'user.email']).strip() or None
4002
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004003 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004004
4005 if args:
4006 if len(args) > 1:
4007 parser.error('Unknown args')
4008 base_branch = args[0]
4009 else:
4010 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004011 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004012
4013 change = cl.GetChange(base_branch, None)
4014 return owners_finder.OwnersFinder(
4015 [f.LocalPath() for f in
4016 cl.GetChange(base_branch, None).AffectedFiles()],
4017 change.RepositoryRoot(), author,
4018 fopen=file, os_path=os.path, glob=glob.glob,
4019 disable_color=options.no_color).run()
4020
4021
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004022def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004023 """Generates a diff command."""
4024 # Generate diff for the current branch's changes.
4025 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4026 upstream_commit, '--' ]
4027
4028 if args:
4029 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004030 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004031 diff_cmd.append(arg)
4032 else:
4033 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004034
4035 return diff_cmd
4036
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004037def MatchingFileType(file_name, extensions):
4038 """Returns true if the file name ends with one of the given extensions."""
4039 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004040
enne@chromium.org555cfe42014-01-29 18:21:39 +00004041@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004042def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004043 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004044 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004045 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004046 parser.add_option('--full', action='store_true',
4047 help='Reformat the full content of all touched files')
4048 parser.add_option('--dry-run', action='store_true',
4049 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004050 parser.add_option('--python', action='store_true',
4051 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004052 parser.add_option('--diff', action='store_true',
4053 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004054 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004055
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004056 # git diff generates paths against the root of the repository. Change
4057 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004058 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004059 if rel_base_path:
4060 os.chdir(rel_base_path)
4061
digit@chromium.org29e47272013-05-17 17:01:46 +00004062 # Grab the merge-base commit, i.e. the upstream commit of the current
4063 # branch when it was created or the last time it was rebased. This is
4064 # to cover the case where the user may have called "git fetch origin",
4065 # moving the origin branch to a newer commit, but hasn't rebased yet.
4066 upstream_commit = None
4067 cl = Changelist()
4068 upstream_branch = cl.GetUpstreamBranch()
4069 if upstream_branch:
4070 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4071 upstream_commit = upstream_commit.strip()
4072
4073 if not upstream_commit:
4074 DieWithError('Could not find base commit for this branch. '
4075 'Are you in detached state?')
4076
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004077 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4078 diff_output = RunGit(changed_files_cmd)
4079 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004080 # Filter out files deleted by this CL
4081 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004082
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004083 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4084 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4085 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004086 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004087
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004088 top_dir = os.path.normpath(
4089 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4090
4091 # Locate the clang-format binary in the checkout
4092 try:
4093 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4094 except clang_format.NotFoundError, e:
4095 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004096
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004097 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4098 # formatted. This is used to block during the presubmit.
4099 return_value = 0
4100
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004101 if clang_diff_files:
4102 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004103 cmd = [clang_format_tool]
4104 if not opts.dry_run and not opts.diff:
4105 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004106 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004107 if opts.diff:
4108 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004109 else:
4110 env = os.environ.copy()
4111 env['PATH'] = str(os.path.dirname(clang_format_tool))
4112 try:
4113 script = clang_format.FindClangFormatScriptInChromiumTree(
4114 'clang-format-diff.py')
4115 except clang_format.NotFoundError, e:
4116 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004117
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004118 cmd = [sys.executable, script, '-p0']
4119 if not opts.dry_run and not opts.diff:
4120 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004121
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004122 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4123 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004124
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004125 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4126 if opts.diff:
4127 sys.stdout.write(stdout)
4128 if opts.dry_run and len(stdout) > 0:
4129 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004130
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004131 # Similar code to above, but using yapf on .py files rather than clang-format
4132 # on C/C++ files
4133 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004134 yapf_tool = gclient_utils.FindExecutable('yapf')
4135 if yapf_tool is None:
4136 DieWithError('yapf not found in PATH')
4137
4138 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004139 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004140 cmd = [yapf_tool]
4141 if not opts.dry_run and not opts.diff:
4142 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004143 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004144 if opts.diff:
4145 sys.stdout.write(stdout)
4146 else:
4147 # TODO(sbc): yapf --lines mode still has some issues.
4148 # https://github.com/google/yapf/issues/154
4149 DieWithError('--python currently only works with --full')
4150
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004151 # Dart's formatter does not have the nice property of only operating on
4152 # modified chunks, so hard code full.
4153 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004154 try:
4155 command = [dart_format.FindDartFmtToolInChromiumTree()]
4156 if not opts.dry_run and not opts.diff:
4157 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004158 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004159
ppi@chromium.org6593d932016-03-03 15:41:15 +00004160 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004161 if opts.dry_run and stdout:
4162 return_value = 2
4163 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004164 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4165 'found in this checkout. Files in other languages are still ' +
4166 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004167
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004168 # Format GN build files. Always run on full build files for canonical form.
4169 if gn_diff_files:
4170 cmd = ['gn', 'format']
4171 if not opts.dry_run and not opts.diff:
4172 cmd.append('--in-place')
4173 for gn_diff_file in gn_diff_files:
4174 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4175 if opts.diff:
4176 sys.stdout.write(stdout)
4177
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004178 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004179
4180
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004181@subcommand.usage('<codereview url or issue id>')
4182def CMDcheckout(parser, args):
4183 """Checks out a branch associated with a given Rietveld issue."""
4184 _, args = parser.parse_args(args)
4185
4186 if len(args) != 1:
4187 parser.print_help()
4188 return 1
4189
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004190 target_issue = ParseIssueNum(args[0])
4191 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004192 parser.print_help()
4193 return 1
4194
4195 key_and_issues = [x.split() for x in RunGit(
4196 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4197 .splitlines()]
4198 branches = []
4199 for key, issue in key_and_issues:
4200 if issue == target_issue:
4201 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4202
4203 if len(branches) == 0:
4204 print 'No branch found for issue %s.' % target_issue
4205 return 1
4206 if len(branches) == 1:
4207 RunGit(['checkout', branches[0]])
4208 else:
4209 print 'Multiple branches match issue %s:' % target_issue
4210 for i in range(len(branches)):
4211 print '%d: %s' % (i, branches[i])
4212 which = raw_input('Choose by index: ')
4213 try:
4214 RunGit(['checkout', branches[int(which)]])
4215 except (IndexError, ValueError):
4216 print 'Invalid selection, not checking out any branch.'
4217 return 1
4218
4219 return 0
4220
4221
maruel@chromium.org29404b52014-09-08 22:58:00 +00004222def CMDlol(parser, args):
4223 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004224 print zlib.decompress(base64.b64decode(
4225 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4226 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4227 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4228 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004229 return 0
4230
4231
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004232class OptionParser(optparse.OptionParser):
4233 """Creates the option parse and add --verbose support."""
4234 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004235 optparse.OptionParser.__init__(
4236 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004237 self.add_option(
4238 '-v', '--verbose', action='count', default=0,
4239 help='Use 2 times for more debugging info')
4240
4241 def parse_args(self, args=None, values=None):
4242 options, args = optparse.OptionParser.parse_args(self, args, values)
4243 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4244 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4245 return options, args
4246
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004247
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004248def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004249 if sys.hexversion < 0x02060000:
4250 print >> sys.stderr, (
4251 '\nYour python version %s is unsupported, please upgrade.\n' %
4252 sys.version.split(' ', 1)[0])
4253 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004254
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004255 # Reload settings.
4256 global settings
4257 settings = Settings()
4258
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004259 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004260 dispatcher = subcommand.CommandDispatcher(__name__)
4261 try:
4262 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004263 except auth.AuthenticationError as e:
4264 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004265 except urllib2.HTTPError, e:
4266 if e.code != 500:
4267 raise
4268 DieWithError(
4269 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4270 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004271 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004272
4273
4274if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004275 # These affect sys.stdout so do it outside of main() to simplify mocks in
4276 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004277 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004278 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004279 try:
4280 sys.exit(main(sys.argv[1:]))
4281 except KeyboardInterrupt:
4282 sys.stderr.write('interrupted\n')
4283 sys.exit(1)