blob: 3c345e4b3b3fe0177d43ce3c4960d7f1dac68c7a [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.orge044c812016-03-24 10:13:29 +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.orge044c812016-03-24 10:13:29 +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.orge044c812016-03-24 10:13:29 +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.orge044c812016-03-24 10:13:29 +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.orge044c812016-03-24 10:13:29 +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.orge044c812016-03-24 10:13:29 +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.orge044c812016-03-24 10:13:29 +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.orge044c812016-03-24 10:13:29 +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
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +0000897 # Automatic selection based on issue number set for a current branch.
898 # Rietveld takes precedence over Gerrit.
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000899 assert not self.issue
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +0000900 # 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
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000909
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +0000910 # No issue is set for this branch, so decide based on repo-wide settings.
911 return self._load_codereview_impl(
tandrii@chromium.org6a3e0022016-03-24 17:26:56 +0000912 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld')
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000913
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000914
915 def GetCCList(self):
916 """Return the users cc'd on this CL.
917
918 Return is a string suitable for passing to gcl with the --cc flag.
919 """
920 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000921 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000922 more_cc = ','.join(self.watchers)
923 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
924 return self.cc
925
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000926 def GetCCListWithoutDefault(self):
927 """Return the users cc'd on this CL excluding default ones."""
928 if self.cc is None:
929 self.cc = ','.join(self.watchers)
930 return self.cc
931
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000932 def SetWatchers(self, watchers):
933 """Set the list of email addresses that should be cc'd based on the changed
934 files in this CL.
935 """
936 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937
938 def GetBranch(self):
939 """Returns the short branch name, e.g. 'master'."""
940 if not self.branch:
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000941 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000942 if not branchref:
943 return None
944 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945 self.branch = ShortBranchName(self.branchref)
946 return self.branch
947
948 def GetBranchRef(self):
949 """Returns the full branch name, e.g. 'refs/heads/master'."""
950 self.GetBranch() # Poke the lazy loader.
951 return self.branchref
952
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000953 @staticmethod
954 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000955 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956 e.g. 'origin', 'refs/heads/master'
957 """
958 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
960 error_ok=True).strip()
961 if upstream_branch:
962 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
963 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000964 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
965 error_ok=True).strip()
966 if upstream_branch:
967 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000969 # Fall back on trying a git-svn upstream branch.
970 if settings.GetIsGitSvn():
971 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000973 # Else, try to guess the origin remote.
974 remote_branches = RunGit(['branch', '-r']).split()
975 if 'origin/master' in remote_branches:
976 # Fall back on origin/master if it exits.
977 remote = 'origin'
978 upstream_branch = 'refs/heads/master'
979 elif 'origin/trunk' in remote_branches:
980 # Fall back on origin/trunk if it exists. Generally a shared
981 # git-svn clone
982 remote = 'origin'
983 upstream_branch = 'refs/heads/trunk'
984 else:
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000985 DieWithError(
986 'Unable to determine default branch to diff against.\n'
987 'Either pass complete "git diff"-style arguments, like\n'
988 ' git cl upload origin/master\n'
989 'or verify this branch is set up to track another \n'
990 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991
992 return remote, upstream_branch
993
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000994 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000995 upstream_branch = self.GetUpstreamBranch()
996 if not BranchExists(upstream_branch):
997 DieWithError('The upstream for the current branch (%s) does not exist '
998 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000999 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001000 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 def GetUpstreamBranch(self):
1003 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001004 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001006 upstream_branch = upstream_branch.replace('refs/heads/',
1007 'refs/remotes/%s/' % remote)
1008 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1009 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 self.upstream_branch = upstream_branch
1011 return self.upstream_branch
1012
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001013 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001014 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001015 remote, branch = None, self.GetBranch()
1016 seen_branches = set()
1017 while branch not in seen_branches:
1018 seen_branches.add(branch)
1019 remote, branch = self.FetchUpstreamTuple(branch)
1020 branch = ShortBranchName(branch)
1021 if remote != '.' or branch.startswith('refs/remotes'):
1022 break
1023 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001024 remotes = RunGit(['remote'], error_ok=True).split()
1025 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001026 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001027 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001028 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001029 logging.warning('Could not determine which remote this change is '
1030 'associated with, so defaulting to "%s". This may '
1031 'not be what you want. You may prevent this message '
1032 'by running "git svn info" as documented here: %s',
1033 self._remote,
1034 GIT_INSTRUCTIONS_URL)
1035 else:
1036 logging.warn('Could not determine which remote this change is '
1037 'associated with. You may prevent this message by '
1038 'running "git svn info" as documented here: %s',
1039 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001040 branch = 'HEAD'
1041 if branch.startswith('refs/remotes'):
1042 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001043 elif branch.startswith('refs/branch-heads/'):
1044 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001045 else:
1046 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001047 return self._remote
1048
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001049 def GitSanityChecks(self, upstream_git_obj):
1050 """Checks git repo status and ensures diff is from local commits."""
1051
sbc@chromium.org79706062015-01-14 21:18:12 +00001052 if upstream_git_obj is None:
1053 if self.GetBranch() is None:
1054 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001055 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001056 else:
1057 print >> sys.stderr, (
1058 'ERROR: no upstream branch')
1059 return False
1060
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 # Verify the commit we're diffing against is in our current branch.
1062 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1063 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1064 if upstream_sha != common_ancestor:
1065 print >> sys.stderr, (
1066 'ERROR: %s is not in the current branch. You may need to rebase '
1067 'your tracking branch' % upstream_sha)
1068 return False
1069
1070 # List the commits inside the diff, and verify they are all local.
1071 commits_in_diff = RunGit(
1072 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1073 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1074 remote_branch = remote_branch.strip()
1075 if code != 0:
1076 _, remote_branch = self.GetRemoteBranch()
1077
1078 commits_in_remote = RunGit(
1079 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1080
1081 common_commits = set(commits_in_diff) & set(commits_in_remote)
1082 if common_commits:
1083 print >> sys.stderr, (
1084 'ERROR: Your diff contains %d commits already in %s.\n'
1085 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1086 'the diff. If you are using a custom git flow, you can override'
1087 ' the reference used for this check with "git config '
1088 'gitcl.remotebranch <git-ref>".' % (
1089 len(common_commits), remote_branch, upstream_git_obj))
1090 return False
1091 return True
1092
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001093 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001094 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001095
1096 Returns None if it is not set.
1097 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001098 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1099 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001100
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001101 def GetGitSvnRemoteUrl(self):
1102 """Return the configured git-svn remote URL parsed from git svn info.
1103
1104 Returns None if it is not set.
1105 """
1106 # URL is dependent on the current directory.
1107 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1108 if data:
1109 keys = dict(line.split(': ', 1) for line in data.splitlines()
1110 if ': ' in line)
1111 return keys.get('URL', None)
1112 return None
1113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114 def GetRemoteUrl(self):
1115 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1116
1117 Returns None if there is no remote.
1118 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001119 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001120 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1121
1122 # If URL is pointing to a local directory, it is probably a git cache.
1123 if os.path.isdir(url):
1124 url = RunGit(['config', 'remote.%s.url' % remote],
1125 error_ok=True,
1126 cwd=url).strip()
1127 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +00001129 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001130 """Returns the issue number as a int or None if not set."""
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +00001131 if self.issue is None and not self.lookedup_issue:
1132 issue = RunGit(['config',
1133 self._codereview_impl.IssueSetting(self.GetBranch())],
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001134 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001135 self.issue = int(issue) or None if issue else None
1136 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 return self.issue
1138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 def GetIssueURL(self):
1140 """Get the URL for a particular issue."""
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001141 issue = self.GetIssue()
1142 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001143 return None
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001144 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 def GetDescription(self, pretty=False):
1147 if not self.has_description:
1148 if self.GetIssue():
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001149 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 self.has_description = True
1151 if pretty:
1152 wrapper = textwrap.TextWrapper()
1153 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1154 return wrapper.fill(self.description)
1155 return self.description
1156
1157 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001158 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001159 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001160 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001162 self.patchset = int(patchset) or None if patchset else None
1163 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 return self.patchset
1165
1166 def SetPatchset(self, patchset):
1167 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001168 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 if patchset:
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001170 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001171 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 else:
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001173 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001174 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001175 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001177 def SetIssue(self, issue=None):
1178 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +00001179 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001180 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001182 self.issue = issue
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001183 RunGit(['config', issue_setting, str(issue)])
1184 codereview_server = self._codereview_impl.GetCodereviewServer()
1185 if codereview_server:
1186 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001188 current_issue = self.GetIssue()
1189 if current_issue:
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001190 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001191 self.issue = None
1192 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001194 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001195 if not self.GitSanityChecks(upstream_branch):
1196 DieWithError('\nGit sanity check failure')
1197
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001198 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001199 if not root:
1200 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001201 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001202
1203 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001204 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001205 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001206 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001207 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001208 except subprocess2.CalledProcessError:
1209 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001210 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001211 'This branch probably doesn\'t exist anymore. To reset the\n'
1212 'tracking branch, please run\n'
1213 ' git branch --set-upstream %s trunk\n'
1214 'replacing trunk with origin/master or the relevant branch') %
1215 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001216
maruel@chromium.org52424302012-08-29 15:14:30 +00001217 issue = self.GetIssue()
1218 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001219 if issue:
1220 description = self.GetDescription()
1221 else:
1222 # If the change was never uploaded, use the log messages of all commits
1223 # up to the branch point, as git cl upload will prefill the description
1224 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001225 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1226 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001227
1228 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001229 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001230 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001231 name,
1232 description,
1233 absroot,
1234 files,
1235 issue,
1236 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001237 author,
1238 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001239
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001240 def UpdateDescription(self, description):
1241 self.description = description
1242 return self._codereview_impl.UpdateDescriptionRemote(description)
1243
1244 def RunHook(self, committing, may_prompt, verbose, change):
1245 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1246 try:
1247 return presubmit_support.DoPresubmitChecks(change, committing,
1248 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1249 default_presubmit=None, may_prompt=may_prompt,
1250 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1251 except presubmit_support.PresubmitFailure, e:
1252 DieWithError(
1253 ('%s\nMaybe your depot_tools is out of date?\n'
1254 'If all fails, contact maruel@') % e)
1255
1256 # Forward methods to codereview specific implementation.
1257
1258 def CloseIssue(self):
1259 return self._codereview_impl.CloseIssue()
1260
1261 def GetStatus(self):
1262 return self._codereview_impl.GetStatus()
1263
1264 def GetCodereviewServer(self):
1265 return self._codereview_impl.GetCodereviewServer()
1266
1267 def GetApprovingReviewers(self):
1268 return self._codereview_impl.GetApprovingReviewers()
1269
1270 def GetMostRecentPatchset(self):
1271 return self._codereview_impl.GetMostRecentPatchset()
1272
1273 def __getattr__(self, attr):
1274 # This is because lots of untested code accesses Rietveld-specific stuff
1275 # directly, and it's hard to fix for sure. So, just let it work, and fix
1276 # on a cases by case basis.
1277 return getattr(self._codereview_impl, attr)
1278
1279
1280class _ChangelistCodereviewBase(object):
1281 """Abstract base class encapsulating codereview specifics of a changelist."""
1282 def __init__(self, changelist):
1283 self._changelist = changelist # instance of Changelist
1284
1285 def __getattr__(self, attr):
1286 # Forward methods to changelist.
1287 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1288 # _RietveldChangelistImpl to avoid this hack?
1289 return getattr(self._changelist, attr)
1290
1291 def GetStatus(self):
1292 """Apply a rough heuristic to give a simple summary of an issue's review
1293 or CQ status, assuming adherence to a common workflow.
1294
1295 Returns None if no issue for this branch, or specific string keywords.
1296 """
1297 raise NotImplementedError()
1298
1299 def GetCodereviewServer(self):
1300 """Returns server URL without end slash, like "https://codereview.com"."""
1301 raise NotImplementedError()
1302
1303 def FetchDescription(self):
1304 """Fetches and returns description from the codereview server."""
1305 raise NotImplementedError()
1306
1307 def GetCodereviewServerSetting(self):
1308 """Returns git config setting for the codereview server."""
1309 raise NotImplementedError()
1310
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +00001311 @staticmethod
1312 def IssueSetting(branch):
1313 """Returns name of git config setting which stores issue number for a given
1314 branch."""
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001315 raise NotImplementedError()
1316
1317 def PatchsetSetting(self):
1318 """Returns name of git config setting which stores issue number."""
1319 raise NotImplementedError()
1320
1321 def GetRieveldObjForPresubmit(self):
1322 # This is an unfortunate Rietveld-embeddedness in presubmit.
1323 # For non-Rietveld codereviews, this probably should return a dummy object.
1324 raise NotImplementedError()
1325
1326 def UpdateDescriptionRemote(self, description):
1327 """Update the description on codereview site."""
1328 raise NotImplementedError()
1329
1330 def CloseIssue(self):
1331 """Closes the issue."""
1332 raise NotImplementedError()
1333
1334 def GetApprovingReviewers(self):
1335 """Returns a list of reviewers approving the change.
1336
1337 Note: not necessarily committers.
1338 """
1339 raise NotImplementedError()
1340
1341 def GetMostRecentPatchset(self):
1342 """Returns the most recent patchset number from the codereview site."""
1343 raise NotImplementedError()
1344
1345
1346class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1347 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1348 super(_RietveldChangelistImpl, self).__init__(changelist)
1349 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1350 settings.GetDefaultServerUrl()
1351
1352 self._rietveld_server = rietveld_server
1353 self._auth_config = auth_config
1354 self._props = None
1355 self._rpc_server = None
1356
1357 def GetAuthConfig(self):
1358 return self._auth_config
1359
1360 def GetCodereviewServer(self):
1361 if not self._rietveld_server:
1362 # If we're on a branch then get the server potentially associated
1363 # with that branch.
1364 if self.GetIssue():
1365 rietveld_server_setting = self.GetCodereviewServerSetting()
1366 if rietveld_server_setting:
1367 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1368 ['config', rietveld_server_setting], error_ok=True).strip())
1369 if not self._rietveld_server:
1370 self._rietveld_server = settings.GetDefaultServerUrl()
1371 return self._rietveld_server
1372
1373 def FetchDescription(self):
1374 issue = self.GetIssue()
1375 assert issue
1376 try:
1377 return self.RpcServer().get_description(issue).strip()
1378 except urllib2.HTTPError as e:
1379 if e.code == 404:
1380 DieWithError(
1381 ('\nWhile fetching the description for issue %d, received a '
1382 '404 (not found)\n'
1383 'error. It is likely that you deleted this '
1384 'issue on the server. If this is the\n'
1385 'case, please run\n\n'
1386 ' git cl issue 0\n\n'
1387 'to clear the association with the deleted issue. Then run '
1388 'this command again.') % issue)
1389 else:
1390 DieWithError(
1391 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1392 except urllib2.URLError as e:
1393 print >> sys.stderr, (
1394 'Warning: Failed to retrieve CL description due to network '
1395 'failure.')
1396 return ''
1397
1398 def GetMostRecentPatchset(self):
1399 return self.GetIssueProperties()['patchsets'][-1]
1400
1401 def GetPatchSetDiff(self, issue, patchset):
1402 return self.RpcServer().get(
1403 '/download/issue%s_%s.diff' % (issue, patchset))
1404
1405 def GetIssueProperties(self):
1406 if self._props is None:
1407 issue = self.GetIssue()
1408 if not issue:
1409 self._props = {}
1410 else:
1411 self._props = self.RpcServer().get_issue_properties(issue, True)
1412 return self._props
1413
1414 def GetApprovingReviewers(self):
1415 return get_approving_reviewers(self.GetIssueProperties())
1416
1417 def AddComment(self, message):
1418 return self.RpcServer().add_comment(self.GetIssue(), message)
1419
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001420 def GetStatus(self):
1421 """Apply a rough heuristic to give a simple summary of an issue's review
1422 or CQ status, assuming adherence to a common workflow.
1423
1424 Returns None if no issue for this branch, or one of the following keywords:
1425 * 'error' - error from review tool (including deleted issues)
1426 * 'unsent' - not sent for review
1427 * 'waiting' - waiting for review
1428 * 'reply' - waiting for owner to reply to review
1429 * 'lgtm' - LGTM from at least one approved reviewer
1430 * 'commit' - in the commit queue
1431 * 'closed' - closed
1432 """
1433 if not self.GetIssue():
1434 return None
1435
1436 try:
1437 props = self.GetIssueProperties()
1438 except urllib2.HTTPError:
1439 return 'error'
1440
1441 if props.get('closed'):
1442 # Issue is closed.
1443 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001444 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001445 # Issue is in the commit queue.
1446 return 'commit'
1447
1448 try:
1449 reviewers = self.GetApprovingReviewers()
1450 except urllib2.HTTPError:
1451 return 'error'
1452
1453 if reviewers:
1454 # Was LGTM'ed.
1455 return 'lgtm'
1456
1457 messages = props.get('messages') or []
1458
1459 if not messages:
1460 # No message was sent.
1461 return 'unsent'
1462 if messages[-1]['sender'] != props.get('owner_email'):
1463 # Non-LGTM reply from non-owner
1464 return 'reply'
1465 return 'waiting'
1466
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001467 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001468 return self.RpcServer().update_description(
1469 self.GetIssue(), self.description)
1470
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001472 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001474 def SetFlag(self, flag, value):
1475 """Patchset must match."""
1476 if not self.GetPatchset():
1477 DieWithError('The patchset needs to match. Send another patchset.')
1478 try:
1479 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001480 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001481 except urllib2.HTTPError, e:
1482 if e.code == 404:
1483 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1484 if e.code == 403:
1485 DieWithError(
1486 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1487 'match?') % (self.GetIssue(), self.GetPatchset()))
1488 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001490 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491 """Returns an upload.RpcServer() to access this review's rietveld instance.
1492 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001493 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001494 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001495 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001496 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001497 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +00001499 @staticmethod
1500 def IssueSetting(branch):
1501 return 'branch.%s.rietveldissue' % branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001503 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 """Return the git setting that stores this change's most recent patchset."""
1505 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1506
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001507 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001508 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001509 branch = self.GetBranch()
1510 if branch:
1511 return 'branch.%s.rietveldserver' % branch
1512 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001514 def GetRieveldObjForPresubmit(self):
1515 return self.RpcServer()
1516
1517
1518class _GerritChangelistImpl(_ChangelistCodereviewBase):
1519 def __init__(self, changelist, auth_config=None):
1520 # auth_config is Rietveld thing, kept here to preserve interface only.
1521 super(_GerritChangelistImpl, self).__init__(changelist)
1522 self._change_id = None
1523 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1524 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1525
1526 def _GetGerritHost(self):
1527 # Lazy load of configs.
1528 self.GetCodereviewServer()
1529 return self._gerrit_host
1530
1531 def GetCodereviewServer(self):
1532 if not self._gerrit_server:
1533 # If we're on a branch then get the server potentially associated
1534 # with that branch.
1535 if self.GetIssue():
1536 gerrit_server_setting = self.GetCodereviewServerSetting()
1537 if gerrit_server_setting:
1538 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1539 error_ok=True).strip()
1540 if self._gerrit_server:
1541 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1542 if not self._gerrit_server:
1543 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1544 # has "-review" suffix for lowest level subdomain.
1545 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1546 parts[0] = parts[0] + '-review'
1547 self._gerrit_host = '.'.join(parts)
1548 self._gerrit_server = 'https://%s' % self._gerrit_host
1549 return self._gerrit_server
1550
tandrii@chromium.orgc24bb3b2016-03-24 11:34:58 +00001551 @staticmethod
1552 def IssueSetting(branch):
1553 return 'branch.%s.gerritissue' % branch
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001554
1555 def PatchsetSetting(self):
1556 """Return the git setting that stores this change's most recent patchset."""
1557 return 'branch.%s.gerritpatchset' % self.GetBranch()
1558
1559 def GetCodereviewServerSetting(self):
1560 """Returns the git setting that stores this change's Gerrit server."""
1561 branch = self.GetBranch()
1562 if branch:
1563 return 'branch.%s.gerritserver' % branch
1564 return None
1565
1566 def GetRieveldObjForPresubmit(self):
1567 class ThisIsNotRietveldIssue(object):
1568 def __getattr__(self, attr):
1569 print(
1570 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1571 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1572 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1573 'or use Rietveld for codereview.\n' % attr)
1574 raise NotImplementedError()
1575 return ThisIsNotRietveldIssue()
1576
1577 def GetStatus(self):
1578 # TODO(tandrii)
1579 raise NotImplementedError()
1580
1581 def GetMostRecentPatchset(self):
1582 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1583 ['CURRENT_REVISION'])
1584 return data['revisions'][data['current_revision']]['_number']
1585
1586 def FetchDescription(self):
1587 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1588 ['COMMIT_FOOTERS', 'CURRENT_REVISION'])
1589 return data['revisions'][data['current_revision']]['commit_with_footers']
1590
1591 def UpdateDescriptionRemote(self, description):
1592 # TODO(tandrii)
1593 raise NotImplementedError()
1594
1595 def CloseIssue(self):
tandrii@chromium.org4ab3d0f2016-03-24 12:44:26 +00001596 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001597
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001598
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001599class ChangeDescription(object):
1600 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001601 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001602 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001603
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001604 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001605 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001606
agable@chromium.org42c20792013-09-12 17:34:49 +00001607 @property # www.logilab.org/ticket/89786
1608 def description(self): # pylint: disable=E0202
1609 return '\n'.join(self._description_lines)
1610
1611 def set_description(self, desc):
1612 if isinstance(desc, basestring):
1613 lines = desc.splitlines()
1614 else:
1615 lines = [line.rstrip() for line in desc]
1616 while lines and not lines[0]:
1617 lines.pop(0)
1618 while lines and not lines[-1]:
1619 lines.pop(-1)
1620 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001621
piman@chromium.org336f9122014-09-04 02:16:55 +00001622 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001623 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001624 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001625 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001626 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001627 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001628
agable@chromium.org42c20792013-09-12 17:34:49 +00001629 # Get the set of R= and TBR= lines and remove them from the desciption.
1630 regexp = re.compile(self.R_LINE)
1631 matches = [regexp.match(line) for line in self._description_lines]
1632 new_desc = [l for i, l in enumerate(self._description_lines)
1633 if not matches[i]]
1634 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001635
agable@chromium.org42c20792013-09-12 17:34:49 +00001636 # Construct new unified R= and TBR= lines.
1637 r_names = []
1638 tbr_names = []
1639 for match in matches:
1640 if not match:
1641 continue
1642 people = cleanup_list([match.group(2).strip()])
1643 if match.group(1) == 'TBR':
1644 tbr_names.extend(people)
1645 else:
1646 r_names.extend(people)
1647 for name in r_names:
1648 if name not in reviewers:
1649 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001650 if add_owners_tbr:
1651 owners_db = owners.Database(change.RepositoryRoot(),
1652 fopen=file, os_path=os.path, glob=glob.glob)
1653 all_reviewers = set(tbr_names + reviewers)
1654 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1655 all_reviewers)
1656 tbr_names.extend(owners_db.reviewers_for(missing_files,
1657 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001658 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1659 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1660
1661 # Put the new lines in the description where the old first R= line was.
1662 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1663 if 0 <= line_loc < len(self._description_lines):
1664 if new_tbr_line:
1665 self._description_lines.insert(line_loc, new_tbr_line)
1666 if new_r_line:
1667 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001668 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001669 if new_r_line:
1670 self.append_footer(new_r_line)
1671 if new_tbr_line:
1672 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001673
1674 def prompt(self):
1675 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001676 self.set_description([
1677 '# Enter a description of the change.',
1678 '# This will be displayed on the codereview site.',
1679 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001680 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001681 '--------------------',
1682 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001683
agable@chromium.org42c20792013-09-12 17:34:49 +00001684 regexp = re.compile(self.BUG_LINE)
1685 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001686 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001687 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001688 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001689 if not content:
1690 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001691 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001692
1693 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001694 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1695 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001696 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001697 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001698
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001699 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001700 if self._description_lines:
1701 # Add an empty line if either the last line or the new line isn't a tag.
1702 last_line = self._description_lines[-1]
1703 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1704 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1705 self._description_lines.append('')
1706 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001707
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001708 def get_reviewers(self):
1709 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001710 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1711 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001712 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001713
1714
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001715def get_approving_reviewers(props):
1716 """Retrieves the reviewers that approved a CL from the issue properties with
1717 messages.
1718
1719 Note that the list may contain reviewers that are not committer, thus are not
1720 considered by the CQ.
1721 """
1722 return sorted(
1723 set(
1724 message['sender']
1725 for message in props['messages']
1726 if message['approval'] and message['sender'] in props['reviewers']
1727 )
1728 )
1729
1730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731def FindCodereviewSettingsFile(filename='codereview.settings'):
1732 """Finds the given file starting in the cwd and going up.
1733
1734 Only looks up to the top of the repository unless an
1735 'inherit-review-settings-ok' file exists in the root of the repository.
1736 """
1737 inherit_ok_file = 'inherit-review-settings-ok'
1738 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001739 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1741 root = '/'
1742 while True:
1743 if filename in os.listdir(cwd):
1744 if os.path.isfile(os.path.join(cwd, filename)):
1745 return open(os.path.join(cwd, filename))
1746 if cwd == root:
1747 break
1748 cwd = os.path.dirname(cwd)
1749
1750
1751def LoadCodereviewSettingsFromFile(fileobj):
1752 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001753 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 def SetProperty(name, setting, unset_error_ok=False):
1756 fullname = 'rietveld.' + name
1757 if setting in keyvals:
1758 RunGit(['config', fullname, keyvals[setting]])
1759 else:
1760 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1761
1762 SetProperty('server', 'CODE_REVIEW_SERVER')
1763 # Only server setting is required. Other settings can be absent.
1764 # In that case, we ignore errors raised during option deletion attempt.
1765 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001766 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1768 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001769 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001770 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001771 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1772 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001773 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001774 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001775 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001776 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1777 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001778
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001779 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001780 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001781
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001782 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1783 RunGit(['config', 'gerrit.squash-uploads',
1784 keyvals['GERRIT_SQUASH_UPLOADS']])
1785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1787 #should be of the form
1788 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1789 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1790 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1791 keyvals['ORIGIN_URL_CONFIG']])
1792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001793
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001794def urlretrieve(source, destination):
1795 """urllib is broken for SSL connections via a proxy therefore we
1796 can't use urllib.urlretrieve()."""
1797 with open(destination, 'w') as f:
1798 f.write(urllib2.urlopen(source).read())
1799
1800
ukai@chromium.org712d6102013-11-27 00:52:58 +00001801def hasSheBang(fname):
1802 """Checks fname is a #! script."""
1803 with open(fname) as f:
1804 return f.read(2).startswith('#!')
1805
1806
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001807def DownloadGerritHook(force):
1808 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001809
1810 Args:
1811 force: True to update hooks. False to install hooks if not present.
1812 """
1813 if not settings.GetIsGerrit():
1814 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001815 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001816 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1817 if not os.access(dst, os.X_OK):
1818 if os.path.exists(dst):
1819 if not force:
1820 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001821 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001822 print(
1823 'WARNING: installing Gerrit commit-msg hook.\n'
1824 ' This behavior of git cl will soon be disabled.\n'
1825 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001826 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001827 if not hasSheBang(dst):
1828 DieWithError('Not a script: %s\n'
1829 'You need to download from\n%s\n'
1830 'into .git/hooks/commit-msg and '
1831 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001832 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1833 except Exception:
1834 if os.path.exists(dst):
1835 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001836 DieWithError('\nFailed to download hooks.\n'
1837 'You need to download from\n%s\n'
1838 'into .git/hooks/commit-msg and '
1839 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001840
1841
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001842
1843def GetRietveldCodereviewSettingsInteractively():
1844 """Prompt the user for settings."""
1845 server = settings.GetDefaultServerUrl(error_ok=True)
1846 prompt = 'Rietveld server (host[:port])'
1847 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1848 newserver = ask_for_data(prompt + ':')
1849 if not server and not newserver:
1850 newserver = DEFAULT_SERVER
1851 if newserver:
1852 newserver = gclient_utils.UpgradeToHttps(newserver)
1853 if newserver != server:
1854 RunGit(['config', 'rietveld.server', newserver])
1855
1856 def SetProperty(initial, caption, name, is_url):
1857 prompt = caption
1858 if initial:
1859 prompt += ' ("x" to clear) [%s]' % initial
1860 new_val = ask_for_data(prompt + ':')
1861 if new_val == 'x':
1862 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1863 elif new_val:
1864 if is_url:
1865 new_val = gclient_utils.UpgradeToHttps(new_val)
1866 if new_val != initial:
1867 RunGit(['config', 'rietveld.' + name, new_val])
1868
1869 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1870 SetProperty(settings.GetDefaultPrivateFlag(),
1871 'Private flag (rietveld only)', 'private', False)
1872 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1873 'tree-status-url', False)
1874 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1875 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1876 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1877 'run-post-upload-hook', False)
1878
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001879@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001881 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001882
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001883 print('WARNING: git cl config works for Rietveld only.\n'
1884 'For Gerrit, see http://crbug.com/579160.')
1885 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001886 parser.add_option('--activate-update', action='store_true',
1887 help='activate auto-updating [rietveld] section in '
1888 '.git/config')
1889 parser.add_option('--deactivate-update', action='store_true',
1890 help='deactivate auto-updating [rietveld] section in '
1891 '.git/config')
1892 options, args = parser.parse_args(args)
1893
1894 if options.deactivate_update:
1895 RunGit(['config', 'rietveld.autoupdate', 'false'])
1896 return
1897
1898 if options.activate_update:
1899 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1900 return
1901
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001903 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001904 return 0
1905
1906 url = args[0]
1907 if not url.endswith('codereview.settings'):
1908 url = os.path.join(url, 'codereview.settings')
1909
1910 # Load code review settings and download hooks (if available).
1911 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1912 return 0
1913
1914
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001915def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001916 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001917 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1918 branch = ShortBranchName(branchref)
1919 _, args = parser.parse_args(args)
1920 if not args:
1921 print("Current base-url:")
1922 return RunGit(['config', 'branch.%s.base-url' % branch],
1923 error_ok=False).strip()
1924 else:
1925 print("Setting base-url to %s" % args[0])
1926 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1927 error_ok=False).strip()
1928
1929
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001930def color_for_status(status):
1931 """Maps a Changelist status to color, for CMDstatus and other tools."""
1932 return {
1933 'unsent': Fore.RED,
1934 'waiting': Fore.BLUE,
1935 'reply': Fore.YELLOW,
1936 'lgtm': Fore.GREEN,
1937 'commit': Fore.MAGENTA,
1938 'closed': Fore.CYAN,
1939 'error': Fore.WHITE,
1940 }.get(status, Fore.WHITE)
1941
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001942def fetch_cl_status(branch, auth_config=None):
1943 """Fetches information for an issue and returns (branch, issue, status)."""
1944 cl = Changelist(branchref=branch, auth_config=auth_config)
1945 url = cl.GetIssueURL()
1946 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001947
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001948 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001949 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001950 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001951
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001952 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001953
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001954def get_cl_statuses(
1955 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001956 """Returns a blocking iterable of (branch, issue, color) for given branches.
1957
1958 If fine_grained is true, this will fetch CL statuses from the server.
1959 Otherwise, simply indicate if there's a matching url for the given branches.
1960
1961 If max_processes is specified, it is used as the maximum number of processes
1962 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1963 spawned.
1964 """
1965 # Silence upload.py otherwise it becomes unwieldly.
1966 upload.verbosity = 0
1967
1968 if fine_grained:
1969 # Process one branch synchronously to work through authentication, then
1970 # spawn processes to process all the other branches in parallel.
1971 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001972 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1973 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001974
1975 branches_to_fetch = branches[1:]
1976 pool = ThreadPool(
1977 min(max_processes, len(branches_to_fetch))
1978 if max_processes is not None
1979 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001980 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001981 yield x
1982 else:
1983 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1984 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001985 cl = Changelist(branchref=b, auth_config=auth_config)
1986 url = cl.GetIssueURL()
1987 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001988
rmistry@google.com2dd99862015-06-22 12:22:18 +00001989
1990def upload_branch_deps(cl, args):
1991 """Uploads CLs of local branches that are dependents of the current branch.
1992
1993 If the local branch dependency tree looks like:
1994 test1 -> test2.1 -> test3.1
1995 -> test3.2
1996 -> test2.2 -> test3.3
1997
1998 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1999 run on the dependent branches in this order:
2000 test2.1, test3.1, test3.2, test2.2, test3.3
2001
2002 Note: This function does not rebase your local dependent branches. Use it when
2003 you make a change to the parent branch that will not conflict with its
2004 dependent branches, and you would like their dependencies updated in
2005 Rietveld.
2006 """
2007 if git_common.is_dirty_git_tree('upload-branch-deps'):
2008 return 1
2009
2010 root_branch = cl.GetBranch()
2011 if root_branch is None:
2012 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2013 'Get on a branch!')
2014 if not cl.GetIssue() or not cl.GetPatchset():
2015 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2016 'patchset dependencies without an uploaded CL.')
2017
2018 branches = RunGit(['for-each-ref',
2019 '--format=%(refname:short) %(upstream:short)',
2020 'refs/heads'])
2021 if not branches:
2022 print('No local branches found.')
2023 return 0
2024
2025 # Create a dictionary of all local branches to the branches that are dependent
2026 # on it.
2027 tracked_to_dependents = collections.defaultdict(list)
2028 for b in branches.splitlines():
2029 tokens = b.split()
2030 if len(tokens) == 2:
2031 branch_name, tracked = tokens
2032 tracked_to_dependents[tracked].append(branch_name)
2033
2034 print
2035 print 'The dependent local branches of %s are:' % root_branch
2036 dependents = []
2037 def traverse_dependents_preorder(branch, padding=''):
2038 dependents_to_process = tracked_to_dependents.get(branch, [])
2039 padding += ' '
2040 for dependent in dependents_to_process:
2041 print '%s%s' % (padding, dependent)
2042 dependents.append(dependent)
2043 traverse_dependents_preorder(dependent, padding)
2044 traverse_dependents_preorder(root_branch)
2045 print
2046
2047 if not dependents:
2048 print 'There are no dependent local branches for %s' % root_branch
2049 return 0
2050
2051 print ('This command will checkout all dependent branches and run '
2052 '"git cl upload".')
2053 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2054
andybons@chromium.org962f9462016-02-03 20:00:42 +00002055 # Add a default patchset title to all upload calls in Rietveld.
2056 if not settings.GetIsGerrit():
2057 args.extend(['-t', 'Updated patchset dependency'])
2058
rmistry@google.com2dd99862015-06-22 12:22:18 +00002059 # Record all dependents that failed to upload.
2060 failures = {}
2061 # Go through all dependents, checkout the branch and upload.
2062 try:
2063 for dependent_branch in dependents:
2064 print
2065 print '--------------------------------------'
2066 print 'Running "git cl upload" from %s:' % dependent_branch
2067 RunGit(['checkout', '-q', dependent_branch])
2068 print
2069 try:
2070 if CMDupload(OptionParser(), args) != 0:
2071 print 'Upload failed for %s!' % dependent_branch
2072 failures[dependent_branch] = 1
2073 except: # pylint: disable=W0702
2074 failures[dependent_branch] = 1
2075 print
2076 finally:
2077 # Swap back to the original root branch.
2078 RunGit(['checkout', '-q', root_branch])
2079
2080 print
2081 print 'Upload complete for dependent branches!'
2082 for dependent_branch in dependents:
2083 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2084 print ' %s : %s' % (dependent_branch, upload_status)
2085 print
2086
2087 return 0
2088
2089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002091 """Show status of changelists.
2092
2093 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002094 - Red not sent for review or broken
2095 - Blue waiting for review
2096 - Yellow waiting for you to reply to review
2097 - Green LGTM'ed
2098 - Magenta in the commit queue
2099 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002100
2101 Also see 'git cl comments'.
2102 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002103 parser.add_option('--field',
2104 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002105 parser.add_option('-f', '--fast', action='store_true',
2106 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002107 parser.add_option(
2108 '-j', '--maxjobs', action='store', type=int,
2109 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002110
2111 auth.add_auth_options(parser)
2112 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002113 if args:
2114 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002115 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002118 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 if options.field.startswith('desc'):
2120 print cl.GetDescription()
2121 elif options.field == 'id':
2122 issueid = cl.GetIssue()
2123 if issueid:
2124 print issueid
2125 elif options.field == 'patch':
2126 patchset = cl.GetPatchset()
2127 if patchset:
2128 print patchset
2129 elif options.field == 'url':
2130 url = cl.GetIssueURL()
2131 if url:
2132 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002133 return 0
2134
2135 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2136 if not branches:
2137 print('No local branch found.')
2138 return 0
2139
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002140 changes = (
2141 Changelist(branchref=b, auth_config=auth_config)
2142 for b in branches.splitlines())
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002143 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002144 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002145 alignment = max(5, max(len(b) for b in branches))
2146 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002147 output = get_cl_statuses(branches,
2148 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002149 max_processes=options.maxjobs,
2150 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002151
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002152 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002153 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002154 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002155 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002156 b, i, status = output.next()
2157 branch_statuses[b] = (i, status)
2158 issue_url, status = branch_statuses.pop(branch)
2159 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002160 reset = Fore.RESET
2161 if not sys.stdout.isatty():
2162 color = ''
2163 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002164 status_str = '(%s)' % status if status else ''
2165 print ' %*s : %s%s %s%s' % (
2166 alignment, ShortBranchName(branch), color, issue_url, status_str,
2167 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002168
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002169 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002170 print
2171 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002172 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002173 if not cl.GetIssue():
2174 print 'No issue assigned.'
2175 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002176 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002177 if not options.fast:
2178 print 'Issue description:'
2179 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002180 return 0
2181
2182
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002183def colorize_CMDstatus_doc():
2184 """To be called once in main() to add colors to git cl status help."""
2185 colors = [i for i in dir(Fore) if i[0].isupper()]
2186
2187 def colorize_line(line):
2188 for color in colors:
2189 if color in line.upper():
2190 # Extract whitespaces first and the leading '-'.
2191 indent = len(line) - len(line.lstrip(' ')) + 1
2192 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2193 return line
2194
2195 lines = CMDstatus.__doc__.splitlines()
2196 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2197
2198
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002199@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002201 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002202
2203 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002204 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002205 parser.add_option('-r', '--reverse', action='store_true',
2206 help='Lookup the branch(es) for the specified issues. If '
2207 'no issues are specified, all branches with mapped '
2208 'issues will be listed.')
2209 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002210
dnj@chromium.org406c4402015-03-03 17:22:28 +00002211 if options.reverse:
2212 branches = RunGit(['for-each-ref', 'refs/heads',
2213 '--format=%(refname:short)']).splitlines()
2214
2215 # Reverse issue lookup.
2216 issue_branch_map = {}
2217 for branch in branches:
2218 cl = Changelist(branchref=branch)
2219 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2220 if not args:
2221 args = sorted(issue_branch_map.iterkeys())
2222 for issue in args:
2223 if not issue:
2224 continue
2225 print 'Branch for issue number %s: %s' % (
2226 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2227 else:
2228 cl = Changelist()
2229 if len(args) > 0:
2230 try:
2231 issue = int(args[0])
2232 except ValueError:
2233 DieWithError('Pass a number to set the issue or none to list it.\n'
2234 'Maybe you want to run git cl status?')
2235 cl.SetIssue(issue)
2236 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002237 return 0
2238
2239
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002240def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002241 """Shows or posts review comments for any changelist."""
2242 parser.add_option('-a', '--add-comment', dest='comment',
2243 help='comment to add to an issue')
2244 parser.add_option('-i', dest='issue',
2245 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002246 parser.add_option('-j', '--json-file',
2247 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002248 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002249 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002250 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002251
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002252 issue = None
2253 if options.issue:
2254 try:
2255 issue = int(options.issue)
2256 except ValueError:
2257 DieWithError('A review issue id is expected to be a number')
2258
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002259 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002260
2261 if options.comment:
2262 cl.AddComment(options.comment)
2263 return 0
2264
2265 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002266 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002267 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002268 summary.append({
2269 'date': message['date'],
2270 'lgtm': False,
2271 'message': message['text'],
2272 'not_lgtm': False,
2273 'sender': message['sender'],
2274 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002275 if message['disapproval']:
2276 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002277 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002278 elif message['approval']:
2279 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002280 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002281 elif message['sender'] == data['owner_email']:
2282 color = Fore.MAGENTA
2283 else:
2284 color = Fore.BLUE
2285 print '\n%s%s %s%s' % (
2286 color, message['date'].split('.', 1)[0], message['sender'],
2287 Fore.RESET)
2288 if message['text'].strip():
2289 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002290 if options.json_file:
2291 with open(options.json_file, 'wb') as f:
2292 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002293 return 0
2294
2295
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002296def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002297 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002298 parser.add_option('-d', '--display', action='store_true',
2299 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002300 auth.add_auth_options(parser)
2301 options, _ = parser.parse_args(args)
2302 auth_config = auth.extract_auth_config_from_options(options)
2303 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002304 if not cl.GetIssue():
2305 DieWithError('This branch has no associated changelist.')
2306 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002307 if options.display:
2308 print description.description
2309 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002310 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002311 if cl.GetDescription() != description.description:
2312 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002313 return 0
2314
2315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002316def CreateDescriptionFromLog(args):
2317 """Pulls out the commit log to use as a base for the CL description."""
2318 log_args = []
2319 if len(args) == 1 and not args[0].endswith('.'):
2320 log_args = [args[0] + '..']
2321 elif len(args) == 1 and args[0].endswith('...'):
2322 log_args = [args[0][:-1]]
2323 elif len(args) == 2:
2324 log_args = [args[0] + '..' + args[1]]
2325 else:
2326 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002327 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002328
2329
thestig@chromium.org44202a22014-03-11 19:22:18 +00002330def CMDlint(parser, args):
2331 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002332 parser.add_option('--filter', action='append', metavar='-x,+y',
2333 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002334 auth.add_auth_options(parser)
2335 options, args = parser.parse_args(args)
2336 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002337
2338 # Access to a protected member _XX of a client class
2339 # pylint: disable=W0212
2340 try:
2341 import cpplint
2342 import cpplint_chromium
2343 except ImportError:
2344 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2345 return 1
2346
2347 # Change the current working directory before calling lint so that it
2348 # shows the correct base.
2349 previous_cwd = os.getcwd()
2350 os.chdir(settings.GetRoot())
2351 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002352 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002353 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2354 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002355 if not files:
2356 print "Cannot lint an empty CL"
2357 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002358
2359 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002360 command = args + files
2361 if options.filter:
2362 command = ['--filter=' + ','.join(options.filter)] + command
2363 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002364
2365 white_regex = re.compile(settings.GetLintRegex())
2366 black_regex = re.compile(settings.GetLintIgnoreRegex())
2367 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2368 for filename in filenames:
2369 if white_regex.match(filename):
2370 if black_regex.match(filename):
2371 print "Ignoring file %s" % filename
2372 else:
2373 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2374 extra_check_functions)
2375 else:
2376 print "Skipping file %s" % filename
2377 finally:
2378 os.chdir(previous_cwd)
2379 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2380 if cpplint._cpplint_state.error_count != 0:
2381 return 1
2382 return 0
2383
2384
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002385def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002386 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002387 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002388 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002389 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002390 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002391 auth.add_auth_options(parser)
2392 options, args = parser.parse_args(args)
2393 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002394
sbc@chromium.org71437c02015-04-09 19:29:40 +00002395 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002396 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002397 return 1
2398
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002399 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002400 if args:
2401 base_branch = args[0]
2402 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002403 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002404 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002405
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002406 cl.RunHook(
2407 committing=not options.upload,
2408 may_prompt=False,
2409 verbose=options.verbose,
2410 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002411 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002412
2413
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002414def AddChangeIdToCommitMessage(options, args):
2415 """Re-commits using the current message, assumes the commit hook is in
2416 place.
2417 """
2418 log_desc = options.message or CreateDescriptionFromLog(args)
2419 git_command = ['commit', '--amend', '-m', log_desc]
2420 RunGit(git_command)
2421 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002422 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002423 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002424 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002425 else:
2426 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2427
2428
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002429def GenerateGerritChangeId(message):
2430 """Returns Ixxxxxx...xxx change id.
2431
2432 Works the same way as
2433 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2434 but can be called on demand on all platforms.
2435
2436 The basic idea is to generate git hash of a state of the tree, original commit
2437 message, author/committer info and timestamps.
2438 """
2439 lines = []
2440 tree_hash = RunGitSilent(['write-tree'])
2441 lines.append('tree %s' % tree_hash.strip())
2442 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2443 if code == 0:
2444 lines.append('parent %s' % parent.strip())
2445 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2446 lines.append('author %s' % author.strip())
2447 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2448 lines.append('committer %s' % committer.strip())
2449 lines.append('')
2450 # Note: Gerrit's commit-hook actually cleans message of some lines and
2451 # whitespace. This code is not doing this, but it clearly won't decrease
2452 # entropy.
2453 lines.append(message)
2454 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2455 stdin='\n'.join(lines))
2456 return 'I%s' % change_hash.strip()
2457
2458
piman@chromium.org336f9122014-09-04 02:16:55 +00002459def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002460 """upload the current branch to gerrit."""
2461 # We assume the remote called "origin" is the one we want.
2462 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002463 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002464
2465 remote, remote_branch = cl.GetRemoteBranch()
2466 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2467 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002468
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002469 change_desc = ChangeDescription(
2470 options.message or CreateDescriptionFromLog(args))
2471 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002472 print "\nDescription is empty. Aborting..."
2473 return 1
2474
2475 if options.title:
2476 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002477 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002478
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002479 if options.squash:
2480 # Try to get the message from a previous upload.
2481 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002482 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002483 if not message:
2484 if not options.force:
2485 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002486 if not change_desc.description:
2487 print "Description is empty; aborting."
2488 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002489 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002490 change_ids = git_footers.get_footer_change_id(message)
2491 if len(change_ids) > 1:
2492 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2493 if not change_ids:
2494 message = git_footers.add_footer_change_id(
2495 message, GenerateGerritChangeId(message))
2496 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002497 change_ids = git_footers.get_footer_change_id(message)
2498 assert len(change_ids) == 1
2499
2500 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002501
2502 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2503 if remote is '.':
2504 # If our upstream branch is local, we base our squashed commit on its
2505 # squashed version.
2506 parent = ('refs/heads/git_cl_uploads/' +
2507 scm.GIT.ShortBranchName(upstream_branch))
2508
2509 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2510 # will create additional CLs when uploading.
2511 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2512 RunGitSilent(['rev-parse', parent + ':'])):
2513 print 'Upload upstream branch ' + upstream_branch + ' first.'
2514 return 1
2515 else:
2516 parent = cl.GetCommonAncestorWithUpstream()
2517
2518 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2519 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2520 '-m', message]).strip()
2521 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002522 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002523 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002524 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002525 ref_to_push = 'HEAD'
2526 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002527 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002528
2529 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2530 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002531 if len(commits) > 1:
2532 print('WARNING: This will upload %d commits. Run the following command '
2533 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002534 print('git log %s..%s' % (parent, ref_to_push))
2535 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002536 'commit.')
2537 ask_for_data('About to upload; enter to confirm.')
2538
piman@chromium.org336f9122014-09-04 02:16:55 +00002539 if options.reviewers or options.tbr_owners:
2540 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002541
ukai@chromium.orge8077812012-02-03 03:41:46 +00002542 receive_options = []
2543 cc = cl.GetCCList().split(',')
2544 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002545 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002546 cc = filter(None, cc)
2547 if cc:
2548 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002549 if change_desc.get_reviewers():
2550 receive_options.extend(
2551 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002552
ukai@chromium.orge8077812012-02-03 03:41:46 +00002553 git_command = ['push']
2554 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002555 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002556 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002557 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002558 push_stdout = gclient_utils.CheckCallAndFilter(
2559 ['git'] + git_command,
2560 print_stdout=True,
2561 # Flush after every line: useful for seeing progress when running as
2562 # recipe.
2563 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002564
2565 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002566 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2567 change_numbers = [m.group(1)
2568 for m in map(regex.match, push_stdout.splitlines())
2569 if m]
2570 if len(change_numbers) != 1:
2571 DieWithError(
2572 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2573 'Change-Id: %s') % (len(change_numbers), change_id))
2574 cl.SetIssue(change_numbers[0])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002575 head = RunGit(['rev-parse', 'HEAD']).strip()
2576 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002577 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002578
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002579
wittman@chromium.org455dc922015-01-26 20:15:50 +00002580def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2581 """Computes the remote branch ref to use for the CL.
2582
2583 Args:
2584 remote (str): The git remote for the CL.
2585 remote_branch (str): The git remote branch for the CL.
2586 target_branch (str): The target branch specified by the user.
2587 pending_prefix (str): The pending prefix from the settings.
2588 """
2589 if not (remote and remote_branch):
2590 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002591
wittman@chromium.org455dc922015-01-26 20:15:50 +00002592 if target_branch:
2593 # Cannonicalize branch references to the equivalent local full symbolic
2594 # refs, which are then translated into the remote full symbolic refs
2595 # below.
2596 if '/' not in target_branch:
2597 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2598 else:
2599 prefix_replacements = (
2600 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2601 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2602 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2603 )
2604 match = None
2605 for regex, replacement in prefix_replacements:
2606 match = re.search(regex, target_branch)
2607 if match:
2608 remote_branch = target_branch.replace(match.group(0), replacement)
2609 break
2610 if not match:
2611 # This is a branch path but not one we recognize; use as-is.
2612 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002613 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2614 # Handle the refs that need to land in different refs.
2615 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002616
wittman@chromium.org455dc922015-01-26 20:15:50 +00002617 # Create the true path to the remote branch.
2618 # Does the following translation:
2619 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2620 # * refs/remotes/origin/master -> refs/heads/master
2621 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2622 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2623 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2624 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2625 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2626 'refs/heads/')
2627 elif remote_branch.startswith('refs/remotes/branch-heads'):
2628 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2629 # If a pending prefix exists then replace refs/ with it.
2630 if pending_prefix:
2631 remote_branch = remote_branch.replace('refs/', pending_prefix)
2632 return remote_branch
2633
2634
piman@chromium.org336f9122014-09-04 02:16:55 +00002635def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002636 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002637 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002638 upload_args.extend(['--server', cl.GetCodereviewServer()])
2639 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
2640 upload_args.extend(auth.auth_config_to_command_options(
2641 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002642 if options.emulate_svn_auto_props:
2643 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002644
2645 change_desc = None
2646
pgervais@chromium.org91141372014-01-09 23:27:20 +00002647 if options.email is not None:
2648 upload_args.extend(['--email', options.email])
2649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002650 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002651 if options.title:
2652 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002653 if options.message:
2654 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002655 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002656 print ("This branch is associated with issue %s. "
2657 "Adding patch to that issue." % cl.GetIssue())
2658 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002659 if options.title:
2660 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002661 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002662 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002663 if options.reviewers or options.tbr_owners:
2664 change_desc.update_reviewers(options.reviewers,
2665 options.tbr_owners,
2666 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002667 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002668 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002669
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002670 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002671 print "Description is empty; aborting."
2672 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002673
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002674 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002675 if change_desc.get_reviewers():
2676 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002677 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002678 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002679 DieWithError("Must specify reviewers to send email.")
2680 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002681
2682 # We check this before applying rietveld.private assuming that in
2683 # rietveld.cc only addresses which we can send private CLs to are listed
2684 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2685 # --private is specified explicitly on the command line.
2686 if options.private:
2687 logging.warn('rietveld.cc is ignored since private flag is specified. '
2688 'You need to review and add them manually if necessary.')
2689 cc = cl.GetCCListWithoutDefault()
2690 else:
2691 cc = cl.GetCCList()
2692 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002693 if cc:
2694 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002695
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002696 if options.private or settings.GetDefaultPrivateFlag() == "True":
2697 upload_args.append('--private')
2698
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002699 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002700 if not options.find_copies:
2701 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002702
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002703 # Include the upstream repo's URL in the change -- this is useful for
2704 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002705 remote_url = cl.GetGitBaseUrlFromConfig()
2706 if not remote_url:
2707 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002708 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002709 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002710 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2711 remote_url = (cl.GetRemoteUrl() + '@'
2712 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002713 if remote_url:
2714 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002715 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002716 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2717 settings.GetPendingRefPrefix())
2718 if target_ref:
2719 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002720
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002721 # Look for dependent patchsets. See crbug.com/480453 for more details.
2722 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2723 upstream_branch = ShortBranchName(upstream_branch)
2724 if remote is '.':
2725 # A local branch is being tracked.
2726 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002727 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002728 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002729 print ('Skipping dependency patchset upload because git config '
2730 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002731 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002732 else:
2733 auth_config = auth.extract_auth_config_from_options(options)
2734 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2735 branch_cl_issue_url = branch_cl.GetIssueURL()
2736 branch_cl_issue = branch_cl.GetIssue()
2737 branch_cl_patchset = branch_cl.GetPatchset()
2738 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2739 upload_args.extend(
2740 ['--depends_on_patchset', '%s:%s' % (
2741 branch_cl_issue, branch_cl_patchset)])
2742 print
2743 print ('The current branch (%s) is tracking a local branch (%s) with '
2744 'an associated CL.') % (cl.GetBranch(), local_branch)
2745 print 'Adding %s/#ps%s as a dependency patchset.' % (
2746 branch_cl_issue_url, branch_cl_patchset)
2747 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002748
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002749 project = settings.GetProject()
2750 if project:
2751 upload_args.extend(['--project', project])
2752
rmistry@google.comef966222015-04-07 11:15:01 +00002753 if options.cq_dry_run:
2754 upload_args.extend(['--cq_dry_run'])
2755
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002756 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002757 upload_args = ['upload'] + upload_args + args
2758 logging.info('upload.RealMain(%s)', upload_args)
2759 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002760 issue = int(issue)
2761 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002762 except KeyboardInterrupt:
2763 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002764 except:
2765 # If we got an exception after the user typed a description for their
2766 # change, back up the description before re-raising.
2767 if change_desc:
2768 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2769 print '\nGot exception while uploading -- saving description to %s\n' \
2770 % backup_path
2771 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002772 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002773 backup_file.close()
2774 raise
2775
2776 if not cl.GetIssue():
2777 cl.SetIssue(issue)
2778 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002779
2780 if options.use_commit_queue:
2781 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002782 return 0
2783
2784
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002785def cleanup_list(l):
2786 """Fixes a list so that comma separated items are put as individual items.
2787
2788 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2789 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2790 """
2791 items = sum((i.split(',') for i in l), [])
2792 stripped_items = (i.strip() for i in items)
2793 return sorted(filter(None, stripped_items))
2794
2795
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002796@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002797def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002798 """Uploads the current changelist to codereview.
2799
2800 Can skip dependency patchset uploads for a branch by running:
2801 git config branch.branch_name.skip-deps-uploads True
2802 To unset run:
2803 git config --unset branch.branch_name.skip-deps-uploads
2804 Can also set the above globally by using the --global flag.
2805 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002806 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2807 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002808 parser.add_option('--bypass-watchlists', action='store_true',
2809 dest='bypass_watchlists',
2810 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002811 parser.add_option('-f', action='store_true', dest='force',
2812 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002813 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002814 parser.add_option('-t', dest='title',
2815 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002816 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002817 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002818 help='reviewer email addresses')
2819 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002820 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002821 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002822 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002823 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002824 parser.add_option('--emulate_svn_auto_props',
2825 '--emulate-svn-auto-props',
2826 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002827 dest="emulate_svn_auto_props",
2828 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002829 parser.add_option('-c', '--use-commit-queue', action='store_true',
2830 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002831 parser.add_option('--private', action='store_true',
2832 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002833 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002834 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002835 metavar='TARGET',
2836 help='Apply CL to remote ref TARGET. ' +
2837 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002838 parser.add_option('--squash', action='store_true',
2839 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002840 parser.add_option('--no-squash', action='store_true',
2841 help='Don\'t squash multiple commits into one ' +
2842 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002843 parser.add_option('--email', default=None,
2844 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002845 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2846 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002847 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2848 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002849 help='Send the patchset to do a CQ dry run right after '
2850 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002851 parser.add_option('--dependencies', action='store_true',
2852 help='Uploads CLs of all the local branches that depend on '
2853 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002854
rmistry@google.com2dd99862015-06-22 12:22:18 +00002855 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002856 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002857 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002858 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002859 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002860
sbc@chromium.org71437c02015-04-09 19:29:40 +00002861 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002862 return 1
2863
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002864 options.reviewers = cleanup_list(options.reviewers)
2865 options.cc = cleanup_list(options.cc)
2866
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002867 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002868 if args:
2869 # TODO(ukai): is it ok for gerrit case?
2870 base_branch = args[0]
2871 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002872 if cl.GetBranch() is None:
2873 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2874
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002875 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002876 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002877 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002878
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002879 # Make sure authenticated to Rietveld before running expensive hooks. It is
2880 # a fast, best efforts check. Rietveld still can reject the authentication
2881 # during the actual upload.
2882 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2883 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002884 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002885 if not authenticator.has_cached_credentials():
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002886 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002887
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002888 # Apply watchlists on upload.
2889 change = cl.GetChange(base_branch, None)
2890 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2891 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002892 if not options.bypass_watchlists:
2893 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002894
ukai@chromium.orge8077812012-02-03 03:41:46 +00002895 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002896 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002897 # Set the reviewer list now so that presubmit checks can access it.
2898 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002899 change_description.update_reviewers(options.reviewers,
2900 options.tbr_owners,
2901 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002902 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002903 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002904 may_prompt=not options.force,
2905 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002906 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002907 if not hook_results.should_continue():
2908 return 1
2909 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002910 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002911
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002912 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002913 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002914 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002915 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002916 print ('The last upload made from this repository was patchset #%d but '
2917 'the most recent patchset on the server is #%d.'
2918 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002919 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2920 'from another machine or branch the patch you\'re uploading now '
2921 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002922 ask_for_data('About to upload; enter to confirm.')
2923
iannucci@chromium.org79540052012-10-19 23:15:26 +00002924 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002925 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002926 if options.squash and options.no_squash:
2927 DieWithError('Can only use one of --squash or --no-squash')
2928
2929 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2930 not options.no_squash)
2931
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002932 ret = GerritUpload(options, args, cl, change)
2933 else:
2934 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002935 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002936 git_set_branch_value('last-upload-hash',
2937 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002938 # Run post upload hooks, if specified.
2939 if settings.GetRunPostUploadHook():
2940 presubmit_support.DoPostUploadExecuter(
2941 change,
2942 cl,
2943 settings.GetRoot(),
2944 options.verbose,
2945 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002946
rmistry@google.com2dd99862015-06-22 12:22:18 +00002947 # Upload all dependencies if specified.
2948 if options.dependencies:
2949 print
2950 print '--dependencies has been specified.'
2951 print 'All dependent local branches will be re-uploaded.'
2952 print
2953 # Remove the dependencies flag from args so that we do not end up in a
2954 # loop.
2955 orig_args.remove('--dependencies')
2956 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002957 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002958
2959
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002960def IsSubmoduleMergeCommit(ref):
2961 # When submodules are added to the repo, we expect there to be a single
2962 # non-git-svn merge commit at remote HEAD with a signature comment.
2963 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002964 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002965 return RunGit(cmd) != ''
2966
2967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002968def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002969 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002970
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002971 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972 Updates changelog with metadata (e.g. pointer to review).
2973 Pushes/dcommits the code upstream.
2974 Updates review and closes.
2975 """
2976 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2977 help='bypass upload presubmit hook')
2978 parser.add_option('-m', dest='message',
2979 help="override review description")
2980 parser.add_option('-f', action='store_true', dest='force',
2981 help="force yes to questions (don't prompt)")
2982 parser.add_option('-c', dest='contributor',
2983 help="external contributor for patch (appended to " +
2984 "description and used as author for git). Should be " +
2985 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002986 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002987 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002988 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002989 auth_config = auth.extract_auth_config_from_options(options)
2990
2991 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002992
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002993 current = cl.GetBranch()
2994 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2995 if not settings.GetIsGitSvn() and remote == '.':
2996 print
2997 print 'Attempting to push branch %r into another local branch!' % current
2998 print
2999 print 'Either reparent this branch on top of origin/master:'
3000 print ' git reparent-branch --root'
3001 print
3002 print 'OR run `git rebase-update` if you think the parent branch is already'
3003 print 'committed.'
3004 print
3005 print ' Current parent: %r' % upstream_branch
3006 return 1
3007
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003008 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003009 # Default to merging against our best guess of the upstream branch.
3010 args = [cl.GetUpstreamBranch()]
3011
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003012 if options.contributor:
3013 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3014 print "Please provide contibutor as 'First Last <email@example.com>'"
3015 return 1
3016
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003017 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003018 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003019
sbc@chromium.org71437c02015-04-09 19:29:40 +00003020 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003021 return 1
3022
3023 # This rev-list syntax means "show all commits not in my branch that
3024 # are in base_branch".
3025 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3026 base_branch]).splitlines()
3027 if upstream_commits:
3028 print ('Base branch "%s" has %d commits '
3029 'not in this branch.' % (base_branch, len(upstream_commits)))
3030 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3031 return 1
3032
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003033 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003034 svn_head = None
3035 if cmd == 'dcommit' or base_has_submodules:
3036 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3037 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003039 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003040 # If the base_head is a submodule merge commit, the first parent of the
3041 # base_head should be a git-svn commit, which is what we're interested in.
3042 base_svn_head = base_branch
3043 if base_has_submodules:
3044 base_svn_head += '^1'
3045
3046 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003047 if extra_commits:
3048 print ('This branch has %d additional commits not upstreamed yet.'
3049 % len(extra_commits.splitlines()))
3050 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3051 'before attempting to %s.' % (base_branch, cmd))
3052 return 1
3053
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003054 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003055 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003056 author = None
3057 if options.contributor:
3058 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003059 hook_results = cl.RunHook(
3060 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003061 may_prompt=not options.force,
3062 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003063 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003064 if not hook_results.should_continue():
3065 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003066
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003067 # Check the tree status if the tree status URL is set.
3068 status = GetTreeStatus()
3069 if 'closed' == status:
3070 print('The tree is closed. Please wait for it to reopen. Use '
3071 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3072 return 1
3073 elif 'unknown' == status:
3074 print('Unable to determine tree status. Please verify manually and '
3075 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3076 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003077
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003078 change_desc = ChangeDescription(options.message)
3079 if not change_desc.description and cl.GetIssue():
3080 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003081
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003082 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003083 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003084 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003085 else:
3086 print 'No description set.'
3087 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3088 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003089
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003090 # Keep a separate copy for the commit message, because the commit message
3091 # contains the link to the Rietveld issue, while the Rietveld message contains
3092 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003093 # Keep a separate copy for the commit message.
3094 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003095 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003096
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003097 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003098 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003099 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003100 # after it. Add a period on a new line to circumvent this. Also add a space
3101 # before the period to make sure that Gitiles continues to correctly resolve
3102 # the URL.
3103 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003104 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003105 commit_desc.append_footer('Patch from %s.' % options.contributor)
3106
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003107 print('Description:')
3108 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003109
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003110 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003111 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003112 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003113
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003114 # We want to squash all this branch's commits into one commit with the proper
3115 # description. We do this by doing a "reset --soft" to the base branch (which
3116 # keeps the working copy the same), then dcommitting that. If origin/master
3117 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3118 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003119 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003120 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3121 # Delete the branches if they exist.
3122 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3123 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3124 result = RunGitWithCode(showref_cmd)
3125 if result[0] == 0:
3126 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003127
3128 # We might be in a directory that's present in this branch but not in the
3129 # trunk. Move up to the top of the tree so that git commands that expect a
3130 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003131 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003132 if rel_base_path:
3133 os.chdir(rel_base_path)
3134
3135 # Stuff our change into the merge branch.
3136 # We wrap in a try...finally block so if anything goes wrong,
3137 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003138 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003139 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003140 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003141 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003142 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003143 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003144 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003145 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146 RunGit(
3147 [
3148 'commit', '--author', options.contributor,
3149 '-m', commit_desc.description,
3150 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003151 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003152 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003153 if base_has_submodules:
3154 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3155 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3156 RunGit(['checkout', CHERRY_PICK_BRANCH])
3157 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003158 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003159 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003160 mirror = settings.GetGitMirror(remote)
3161 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003162 pending_prefix = settings.GetPendingRefPrefix()
3163 if not pending_prefix or branch.startswith(pending_prefix):
3164 # If not using refs/pending/heads/* at all, or target ref is already set
3165 # to pending, then push to the target ref directly.
3166 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003167 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003168 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003169 else:
3170 # Cherry-pick the change on top of pending ref and then push it.
3171 assert branch.startswith('refs/'), branch
3172 assert pending_prefix[-1] == '/', pending_prefix
3173 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003174 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003175 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003176 if retcode == 0:
3177 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003178 else:
3179 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003180 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003181 'svn', 'dcommit',
3182 '-C%s' % options.similarity,
3183 '--no-rebase', '--rmdir',
3184 ]
3185 if settings.GetForceHttpsCommitUrl():
3186 # Allow forcing https commit URLs for some projects that don't allow
3187 # committing to http URLs (like Google Code).
3188 remote_url = cl.GetGitSvnRemoteUrl()
3189 if urlparse.urlparse(remote_url).scheme == 'http':
3190 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003191 cmd_args.append('--commit-url=%s' % remote_url)
3192 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003193 if 'Committed r' in output:
3194 revision = re.match(
3195 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3196 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003197 finally:
3198 # And then swap back to the original branch and clean up.
3199 RunGit(['checkout', '-q', cl.GetBranch()])
3200 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003201 if base_has_submodules:
3202 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003203
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003204 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003205 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003206 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003207
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003208 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003209 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003210 try:
3211 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3212 # We set pushed_to_pending to False, since it made it all the way to the
3213 # real ref.
3214 pushed_to_pending = False
3215 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003216 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003218 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003219 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003220 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003221 if not to_pending:
3222 if viewvc_url and revision:
3223 change_desc.append_footer(
3224 'Committed: %s%s' % (viewvc_url, revision))
3225 elif revision:
3226 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003227 print ('Closing issue '
3228 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003229 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003230 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003231 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003232 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003233 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003234 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003235 if options.bypass_hooks:
3236 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3237 else:
3238 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003239 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003240 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003241
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003242 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003243 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3244 print 'The commit is in the pending queue (%s).' % pending_ref
3245 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003246 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003247 'footer.' % branch)
3248
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003249 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3250 if os.path.isfile(hook):
3251 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003252
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003253 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254
3255
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003256def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3257 print
3258 print 'Waiting for commit to be landed on %s...' % real_ref
3259 print '(If you are impatient, you may Ctrl-C once without harm)'
3260 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3261 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003262 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003263
3264 loop = 0
3265 while True:
3266 sys.stdout.write('fetching (%d)... \r' % loop)
3267 sys.stdout.flush()
3268 loop += 1
3269
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003270 if mirror:
3271 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003272 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3273 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3274 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3275 for commit in commits.splitlines():
3276 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3277 print 'Found commit on %s' % real_ref
3278 return commit
3279
3280 current_rev = to_rev
3281
3282
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003283def PushToGitPending(remote, pending_ref, upstream_ref):
3284 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3285
3286 Returns:
3287 (retcode of last operation, output log of last operation).
3288 """
3289 assert pending_ref.startswith('refs/'), pending_ref
3290 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3291 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3292 code = 0
3293 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003294 max_attempts = 3
3295 attempts_left = max_attempts
3296 while attempts_left:
3297 if attempts_left != max_attempts:
3298 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3299 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003300
3301 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003302 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003303 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003304 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003305 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003306 print 'Fetch failed with exit code %d.' % code
3307 if out.strip():
3308 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003309 continue
3310
3311 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003312 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003313 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003314 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003315 if code:
3316 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003317 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3318 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003319 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3320 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003321 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003322 return code, out
3323
3324 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003325 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003326 code, out = RunGitWithCode(
3327 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3328 if code == 0:
3329 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003330 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003331 return code, out
3332
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003333 print 'Push failed with exit code %d.' % code
3334 if out.strip():
3335 print out.strip()
3336 if IsFatalPushFailure(out):
3337 print (
3338 'Fatal push error. Make sure your .netrc credentials and git '
3339 'user.email are correct and you have push access to the repo.')
3340 return code, out
3341
3342 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003343 return code, out
3344
3345
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003346def IsFatalPushFailure(push_stdout):
3347 """True if retrying push won't help."""
3348 return '(prohibited by Gerrit)' in push_stdout
3349
3350
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003351@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003352def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003353 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003354 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003355 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003356 # If it looks like previous commits were mirrored with git-svn.
3357 message = """This repository appears to be a git-svn mirror, but no
3358upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3359 else:
3360 message = """This doesn't appear to be an SVN repository.
3361If your project has a true, writeable git repository, you probably want to run
3362'git cl land' instead.
3363If your project has a git mirror of an upstream SVN master, you probably need
3364to run 'git svn init'.
3365
3366Using the wrong command might cause your commit to appear to succeed, and the
3367review to be closed, without actually landing upstream. If you choose to
3368proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003369 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003370 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003371 return SendUpstream(parser, args, 'dcommit')
3372
3373
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003374@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003375def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003376 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003377 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003378 print('This appears to be an SVN repository.')
3379 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003380 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003381 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003382 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003383
3384
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003385def ParseIssueNum(arg):
3386 """Parses the issue number from args if present otherwise returns None."""
3387 if re.match(r'\d+', arg):
3388 return arg
3389 if arg.startswith('http'):
3390 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3391 return None
3392
3393
3394@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003395def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003396 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003397 parser.add_option('-b', dest='newbranch',
3398 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003399 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003400 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003401 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3402 help='Change to the directory DIR immediately, '
3403 'before doing anything else.')
3404 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003405 help='failed patches spew .rej files rather than '
3406 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003407 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3408 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003409
3410 group = optparse.OptionGroup(parser,
3411 """Options for continuing work on the current issue uploaded
3412from a different clone (e.g. different machine). Must be used independently from
3413the other options. No issue number should be specified, and the branch must have
3414an issue number associated with it""")
3415 group.add_option('--reapply', action='store_true',
3416 dest='reapply',
3417 help="""Reset the branch and reapply the issue.
3418CAUTION: This will undo any local changes in this branch""")
3419
3420 group.add_option('--pull', action='store_true', dest='pull',
3421 help="Performs a pull before reapplying.")
3422 parser.add_option_group(group)
3423
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003424 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003425 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003426 auth_config = auth.extract_auth_config_from_options(options)
3427
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003428 issue_arg = None
3429 if options.reapply :
3430 if len(args) > 0:
3431 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003432
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003433 cl = Changelist()
3434 issue_arg = cl.GetIssue()
3435 upstream = cl.GetUpstreamBranch()
3436 if upstream == None:
3437 parser.error("No upstream branch specified. Cannot reset branch")
3438
3439 RunGit(['reset', '--hard', upstream])
3440 if options.pull:
3441 RunGit(['pull'])
3442 else:
3443 if len(args) != 1:
3444 parser.error("Must specify issue number")
3445
3446 issue_arg = ParseIssueNum(args[0])
3447
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003448 # The patch URL works because ParseIssueNum won't do any substitution
3449 # as the re.sub pattern fails to match and just returns it.
3450 if issue_arg == None:
3451 parser.print_help()
3452 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003453
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003454 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003455 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003456 return 1
3457
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003458 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003459 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003460
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003461 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003462 if options.reapply:
3463 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003464 if options.force:
3465 RunGit(['branch', '-D', options.newbranch],
3466 stderr=subprocess2.PIPE, error_ok=True)
3467 RunGit(['checkout', '-b', options.newbranch,
3468 Changelist().GetUpstreamBranch()])
3469
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003470 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003471 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003472
3473
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003474def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003475 # PatchIssue should never be called with a dirty tree. It is up to the
3476 # caller to check this, but just in case we assert here since the
3477 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003478 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003479
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003480 # TODO(tandrii): implement for Gerrit.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003481 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003482 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003483 issue = int(issue_arg)
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003484 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003485 patchset = cl.GetMostRecentPatchset()
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003486 patch_data = cl._codereview_impl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003487 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003488 # Assume it's a URL to the patch. Default to https.
3489 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003490 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003491 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003492 DieWithError('Must pass an issue ID or full URL for '
3493 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003494 issue = int(match.group(2))
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003495 cl = Changelist(issue=issue, codereview='rietveld',
3496 rietvled_server=match.group(1), auth_config=auth_config)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003497 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003498 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003499
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003500 # Switch up to the top-level directory, if necessary, in preparation for
3501 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003502 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003503 if top:
3504 os.chdir(top)
3505
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003506 # Git patches have a/ at the beginning of source paths. We strip that out
3507 # with a sed script rather than the -p flag to patch so we can feed either
3508 # Git or svn-style patches into the same apply command.
3509 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003510 try:
3511 patch_data = subprocess2.check_output(
3512 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3513 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003514 DieWithError('Git patch mungling failed.')
3515 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003516
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003517 # We use "git apply" to apply the patch instead of "patch" so that we can
3518 # pick up file adds.
3519 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003520 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003521 if directory:
3522 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003523 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003524 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003525 elif IsGitVersionAtLeast('1.7.12'):
3526 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003527 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003528 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003529 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003530 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003531 print 'Failed to apply the patch'
3532 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533
3534 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003535 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003536 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3537 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003538 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3539 % {'i': issue, 'p': patchset})])
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003540 cl = Changelist(codereview='rietveld', auth_config=auth_config,
3541 rietveld_server=cl.GetCodereviewServer())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003542 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003543 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003544 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545 else:
3546 print "Patch applied to index."
3547 return 0
3548
3549
3550def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003551 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003552 # Provide a wrapper for git svn rebase to help avoid accidental
3553 # git svn dcommit.
3554 # It's the only command that doesn't use parser at all since we just defer
3555 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003556
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003557 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003558
3559
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003560def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561 """Fetches the tree status and returns either 'open', 'closed',
3562 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003563 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564 if url:
3565 status = urllib2.urlopen(url).read().lower()
3566 if status.find('closed') != -1 or status == '0':
3567 return 'closed'
3568 elif status.find('open') != -1 or status == '1':
3569 return 'open'
3570 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003571 return 'unset'
3572
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003573
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003574def GetTreeStatusReason():
3575 """Fetches the tree status from a json url and returns the message
3576 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003577 url = settings.GetTreeStatusUrl()
3578 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003579 connection = urllib2.urlopen(json_url)
3580 status = json.loads(connection.read())
3581 connection.close()
3582 return status['message']
3583
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003584
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003585def GetBuilderMaster(bot_list):
3586 """For a given builder, fetch the master from AE if available."""
3587 map_url = 'https://builders-map.appspot.com/'
3588 try:
3589 master_map = json.load(urllib2.urlopen(map_url))
3590 except urllib2.URLError as e:
3591 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3592 (map_url, e))
3593 except ValueError as e:
3594 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3595 if not master_map:
3596 return None, 'Failed to build master map.'
3597
3598 result_master = ''
3599 for bot in bot_list:
3600 builder = bot.split(':', 1)[0]
3601 master_list = master_map.get(builder, [])
3602 if not master_list:
3603 return None, ('No matching master for builder %s.' % builder)
3604 elif len(master_list) > 1:
3605 return None, ('The builder name %s exists in multiple masters %s.' %
3606 (builder, master_list))
3607 else:
3608 cur_master = master_list[0]
3609 if not result_master:
3610 result_master = cur_master
3611 elif result_master != cur_master:
3612 return None, 'The builders do not belong to the same master.'
3613 return result_master, None
3614
3615
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003616def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003617 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003618 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619 status = GetTreeStatus()
3620 if 'unset' == status:
3621 print 'You must configure your tree status URL by running "git cl config".'
3622 return 2
3623
3624 print "The tree is %s" % status
3625 print
3626 print GetTreeStatusReason()
3627 if status != 'open':
3628 return 1
3629 return 0
3630
3631
maruel@chromium.org15192402012-09-06 12:38:29 +00003632def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003633 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003634 group = optparse.OptionGroup(parser, "Try job options")
3635 group.add_option(
3636 "-b", "--bot", action="append",
3637 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3638 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003639 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003640 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003641 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003642 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003643 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003644 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003645 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003646 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003647 "-r", "--revision",
3648 help="Revision to use for the try job; default: the "
3649 "revision will be determined by the try server; see "
3650 "its waterfall for more info")
3651 group.add_option(
3652 "-c", "--clobber", action="store_true", default=False,
3653 help="Force a clobber before building; e.g. don't do an "
3654 "incremental build")
3655 group.add_option(
3656 "--project",
3657 help="Override which project to use. Projects are defined "
3658 "server-side to define what default bot set to use")
3659 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003660 "-p", "--property", dest="properties", action="append", default=[],
3661 help="Specify generic properties in the form -p key1=value1 -p "
3662 "key2=value2 etc (buildbucket only). The value will be treated as "
3663 "json if decodable, or as string otherwise.")
3664 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003665 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003666 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003667 "--use-rietveld", action="store_true", default=False,
3668 help="Use Rietveld to trigger try jobs.")
3669 group.add_option(
3670 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3671 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003672 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003673 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003674 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003675 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003676
machenbach@chromium.org45453142015-09-15 08:45:22 +00003677 if options.use_rietveld and options.properties:
3678 parser.error('Properties can only be specified with buildbucket')
3679
3680 # Make sure that all properties are prop=value pairs.
3681 bad_params = [x for x in options.properties if '=' not in x]
3682 if bad_params:
3683 parser.error('Got properties with missing "=": %s' % bad_params)
3684
maruel@chromium.org15192402012-09-06 12:38:29 +00003685 if args:
3686 parser.error('Unknown arguments: %s' % args)
3687
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003688 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003689 if not cl.GetIssue():
3690 parser.error('Need to upload first')
3691
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003692 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003693 if props.get('closed'):
3694 parser.error('Cannot send tryjobs for a closed CL')
3695
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003696 if props.get('private'):
3697 parser.error('Cannot use trybots with private issue')
3698
maruel@chromium.org15192402012-09-06 12:38:29 +00003699 if not options.name:
3700 options.name = cl.GetBranch()
3701
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003702 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003703 options.master, err_msg = GetBuilderMaster(options.bot)
3704 if err_msg:
3705 parser.error('Tryserver master cannot be found because: %s\n'
3706 'Please manually specify the tryserver master'
3707 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003708
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003709 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003710 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003711 if not options.bot:
3712 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003713
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003714 # Get try masters from PRESUBMIT.py files.
3715 masters = presubmit_support.DoGetTryMasters(
3716 change,
3717 change.LocalPaths(),
3718 settings.GetRoot(),
3719 None,
3720 None,
3721 options.verbose,
3722 sys.stdout)
3723 if masters:
3724 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003725
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003726 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3727 options.bot = presubmit_support.DoGetTrySlaves(
3728 change,
3729 change.LocalPaths(),
3730 settings.GetRoot(),
3731 None,
3732 None,
3733 options.verbose,
3734 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003735
3736 if not options.bot:
3737 # Get try masters from cq.cfg if any.
3738 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3739 # location.
3740 cq_cfg = os.path.join(change.RepositoryRoot(),
3741 'infra', 'config', 'cq.cfg')
3742 if os.path.exists(cq_cfg):
3743 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003744 cq_masters = commit_queue.get_master_builder_map(
3745 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003746 for master, builders in cq_masters.iteritems():
3747 for builder in builders:
3748 # Skip presubmit builders, because these will fail without LGTM.
3749 if 'presubmit' not in builder.lower():
3750 masters.setdefault(master, {})[builder] = ['defaulttests']
3751 if masters:
3752 return masters
3753
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003754 if not options.bot:
3755 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003756
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003757 builders_and_tests = {}
3758 # TODO(machenbach): The old style command-line options don't support
3759 # multiple try masters yet.
3760 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3761 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3762
3763 for bot in old_style:
3764 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003765 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003766 elif ',' in bot:
3767 parser.error('Specify one bot per --bot flag')
3768 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003769 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003770
3771 for bot, tests in new_style:
3772 builders_and_tests.setdefault(bot, []).extend(tests)
3773
3774 # Return a master map with one master to be backwards compatible. The
3775 # master name defaults to an empty string, which will cause the master
3776 # not to be set on rietveld (deprecated).
3777 return {options.master: builders_and_tests}
3778
3779 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003780
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003781 for builders in masters.itervalues():
3782 if any('triggered' in b for b in builders):
3783 print >> sys.stderr, (
3784 'ERROR You are trying to send a job to a triggered bot. This type of'
3785 ' bot requires an\ninitial job from a parent (usually a builder). '
3786 'Instead send your job to the parent.\n'
3787 'Bot list: %s' % builders)
3788 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003789
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003790 patchset = cl.GetMostRecentPatchset()
3791 if patchset and patchset != cl.GetPatchset():
3792 print(
3793 '\nWARNING Mismatch between local config and server. Did a previous '
3794 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3795 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003796 if options.luci:
3797 trigger_luci_job(cl, masters, options)
3798 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003799 try:
3800 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3801 except BuildbucketResponseException as ex:
3802 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003803 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003804 except Exception as e:
3805 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3806 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3807 e, stacktrace)
3808 return 1
3809 else:
3810 try:
3811 cl.RpcServer().trigger_distributed_try_jobs(
3812 cl.GetIssue(), patchset, options.name, options.clobber,
3813 options.revision, masters)
3814 except urllib2.HTTPError as e:
3815 if e.code == 404:
3816 print('404 from rietveld; '
3817 'did you mean to use "git try" instead of "git cl try"?')
3818 return 1
3819 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003820
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003821 for (master, builders) in sorted(masters.iteritems()):
3822 if master:
3823 print 'Master: %s' % master
3824 length = max(len(builder) for builder in builders)
3825 for builder in sorted(builders):
3826 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003827 return 0
3828
3829
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003830def CMDtry_results(parser, args):
3831 group = optparse.OptionGroup(parser, "Try job results options")
3832 group.add_option(
3833 "-p", "--patchset", type=int, help="patchset number if not current.")
3834 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003835 "--print-master", action='store_true', help="print master name as well.")
3836 group.add_option(
3837 "--color", action='store_true', default=sys.stdout.isatty(),
3838 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003839 group.add_option(
3840 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3841 help="Host of buildbucket. The default host is %default.")
3842 parser.add_option_group(group)
3843 auth.add_auth_options(parser)
3844 options, args = parser.parse_args(args)
3845 if args:
3846 parser.error('Unrecognized args: %s' % ' '.join(args))
3847
3848 auth_config = auth.extract_auth_config_from_options(options)
3849 cl = Changelist(auth_config=auth_config)
3850 if not cl.GetIssue():
3851 parser.error('Need to upload first')
3852
3853 if not options.patchset:
3854 options.patchset = cl.GetMostRecentPatchset()
3855 if options.patchset and options.patchset != cl.GetPatchset():
3856 print(
3857 '\nWARNING Mismatch between local config and server. Did a previous '
3858 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3859 'Continuing using\npatchset %s.\n' % options.patchset)
3860 try:
3861 jobs = fetch_try_jobs(auth_config, cl, options)
3862 except BuildbucketResponseException as ex:
3863 print 'Buildbucket error: %s' % ex
3864 return 1
3865 except Exception as e:
3866 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3867 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3868 e, stacktrace)
3869 return 1
3870 print_tryjobs(options, jobs)
3871 return 0
3872
3873
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003874@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003875def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003876 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003877 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003878 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003879 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003881 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003882 if args:
3883 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003884 branch = cl.GetBranch()
3885 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003886 cl = Changelist()
3887 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003888
3889 # Clear configured merge-base, if there is one.
3890 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003891 else:
3892 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003893 return 0
3894
3895
thestig@chromium.org00858c82013-12-02 23:08:03 +00003896def CMDweb(parser, args):
3897 """Opens the current CL in the web browser."""
3898 _, args = parser.parse_args(args)
3899 if args:
3900 parser.error('Unrecognized args: %s' % ' '.join(args))
3901
3902 issue_url = Changelist().GetIssueURL()
3903 if not issue_url:
3904 print >> sys.stderr, 'ERROR No issue to open'
3905 return 1
3906
3907 webbrowser.open(issue_url)
3908 return 0
3909
3910
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003911def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003912 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003913 auth.add_auth_options(parser)
3914 options, args = parser.parse_args(args)
3915 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003916 if args:
3917 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003918 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003919 props = cl.GetIssueProperties()
3920 if props.get('private'):
3921 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003922 cl.SetFlag('commit', '1')
3923 return 0
3924
3925
groby@chromium.org411034a2013-02-26 15:12:01 +00003926def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003927 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003928 auth.add_auth_options(parser)
3929 options, args = parser.parse_args(args)
3930 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003931 if args:
3932 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003933 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003934 # Ensure there actually is an issue to close.
3935 cl.GetDescription()
3936 cl.CloseIssue()
3937 return 0
3938
3939
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003940def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003941 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003942 auth.add_auth_options(parser)
3943 options, args = parser.parse_args(args)
3944 auth_config = auth.extract_auth_config_from_options(options)
3945 if args:
3946 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003947
3948 # Uncommitted (staged and unstaged) changes will be destroyed by
3949 # "git reset --hard" if there are merging conflicts in PatchIssue().
3950 # Staged changes would be committed along with the patch from last
3951 # upload, hence counted toward the "last upload" side in the final
3952 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003953 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003954 return 1
3955
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003956 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003957 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003958 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003959 if not issue:
3960 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003961 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003962 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003963
3964 # Create a new branch based on the merge-base
3965 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3966 try:
3967 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003968 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003969 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003970 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003971 return rtn
3972
wychen@chromium.org06928532015-02-03 02:11:29 +00003973 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003974 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003975 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003976 finally:
3977 RunGit(['checkout', '-q', branch])
3978 RunGit(['branch', '-D', TMP_BRANCH])
3979
3980 return 0
3981
3982
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003983def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003984 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003985 parser.add_option(
3986 '--no-color',
3987 action='store_true',
3988 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003989 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003990 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003991 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003992
3993 author = RunGit(['config', 'user.email']).strip() or None
3994
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003995 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003996
3997 if args:
3998 if len(args) > 1:
3999 parser.error('Unknown args')
4000 base_branch = args[0]
4001 else:
4002 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004003 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004004
4005 change = cl.GetChange(base_branch, None)
4006 return owners_finder.OwnersFinder(
4007 [f.LocalPath() for f in
4008 cl.GetChange(base_branch, None).AffectedFiles()],
4009 change.RepositoryRoot(), author,
4010 fopen=file, os_path=os.path, glob=glob.glob,
4011 disable_color=options.no_color).run()
4012
4013
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004014def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004015 """Generates a diff command."""
4016 # Generate diff for the current branch's changes.
4017 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4018 upstream_commit, '--' ]
4019
4020 if args:
4021 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004022 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004023 diff_cmd.append(arg)
4024 else:
4025 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004026
4027 return diff_cmd
4028
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004029def MatchingFileType(file_name, extensions):
4030 """Returns true if the file name ends with one of the given extensions."""
4031 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004032
enne@chromium.org555cfe42014-01-29 18:21:39 +00004033@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004034def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004035 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004036 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004037 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004038 parser.add_option('--full', action='store_true',
4039 help='Reformat the full content of all touched files')
4040 parser.add_option('--dry-run', action='store_true',
4041 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004042 parser.add_option('--python', action='store_true',
4043 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004044 parser.add_option('--diff', action='store_true',
4045 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004046 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004047
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004048 # git diff generates paths against the root of the repository. Change
4049 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004050 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004051 if rel_base_path:
4052 os.chdir(rel_base_path)
4053
digit@chromium.org29e47272013-05-17 17:01:46 +00004054 # Grab the merge-base commit, i.e. the upstream commit of the current
4055 # branch when it was created or the last time it was rebased. This is
4056 # to cover the case where the user may have called "git fetch origin",
4057 # moving the origin branch to a newer commit, but hasn't rebased yet.
4058 upstream_commit = None
4059 cl = Changelist()
4060 upstream_branch = cl.GetUpstreamBranch()
4061 if upstream_branch:
4062 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4063 upstream_commit = upstream_commit.strip()
4064
4065 if not upstream_commit:
4066 DieWithError('Could not find base commit for this branch. '
4067 'Are you in detached state?')
4068
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004069 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4070 diff_output = RunGit(changed_files_cmd)
4071 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004072 # Filter out files deleted by this CL
4073 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004074
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004075 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4076 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4077 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004078 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004079
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004080 top_dir = os.path.normpath(
4081 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4082
4083 # Locate the clang-format binary in the checkout
4084 try:
4085 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4086 except clang_format.NotFoundError, e:
4087 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004088
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004089 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4090 # formatted. This is used to block during the presubmit.
4091 return_value = 0
4092
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004093 if clang_diff_files:
4094 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004095 cmd = [clang_format_tool]
4096 if not opts.dry_run and not opts.diff:
4097 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004098 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004099 if opts.diff:
4100 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004101 else:
4102 env = os.environ.copy()
4103 env['PATH'] = str(os.path.dirname(clang_format_tool))
4104 try:
4105 script = clang_format.FindClangFormatScriptInChromiumTree(
4106 'clang-format-diff.py')
4107 except clang_format.NotFoundError, e:
4108 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004109
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004110 cmd = [sys.executable, script, '-p0']
4111 if not opts.dry_run and not opts.diff:
4112 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004113
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004114 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4115 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004116
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004117 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4118 if opts.diff:
4119 sys.stdout.write(stdout)
4120 if opts.dry_run and len(stdout) > 0:
4121 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004122
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004123 # Similar code to above, but using yapf on .py files rather than clang-format
4124 # on C/C++ files
4125 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004126 yapf_tool = gclient_utils.FindExecutable('yapf')
4127 if yapf_tool is None:
4128 DieWithError('yapf not found in PATH')
4129
4130 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004131 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004132 cmd = [yapf_tool]
4133 if not opts.dry_run and not opts.diff:
4134 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004135 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004136 if opts.diff:
4137 sys.stdout.write(stdout)
4138 else:
4139 # TODO(sbc): yapf --lines mode still has some issues.
4140 # https://github.com/google/yapf/issues/154
4141 DieWithError('--python currently only works with --full')
4142
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004143 # Dart's formatter does not have the nice property of only operating on
4144 # modified chunks, so hard code full.
4145 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004146 try:
4147 command = [dart_format.FindDartFmtToolInChromiumTree()]
4148 if not opts.dry_run and not opts.diff:
4149 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004150 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004151
ppi@chromium.org6593d932016-03-03 15:41:15 +00004152 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004153 if opts.dry_run and stdout:
4154 return_value = 2
4155 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004156 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4157 'found in this checkout. Files in other languages are still ' +
4158 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004159
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004160 # Format GN build files. Always run on full build files for canonical form.
4161 if gn_diff_files:
4162 cmd = ['gn', 'format']
4163 if not opts.dry_run and not opts.diff:
4164 cmd.append('--in-place')
4165 for gn_diff_file in gn_diff_files:
4166 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4167 if opts.diff:
4168 sys.stdout.write(stdout)
4169
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004170 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004171
4172
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004173@subcommand.usage('<codereview url or issue id>')
4174def CMDcheckout(parser, args):
4175 """Checks out a branch associated with a given Rietveld issue."""
4176 _, args = parser.parse_args(args)
4177
4178 if len(args) != 1:
4179 parser.print_help()
4180 return 1
4181
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004182 target_issue = ParseIssueNum(args[0])
4183 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004184 parser.print_help()
4185 return 1
4186
4187 key_and_issues = [x.split() for x in RunGit(
4188 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4189 .splitlines()]
4190 branches = []
4191 for key, issue in key_and_issues:
4192 if issue == target_issue:
4193 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4194
4195 if len(branches) == 0:
4196 print 'No branch found for issue %s.' % target_issue
4197 return 1
4198 if len(branches) == 1:
4199 RunGit(['checkout', branches[0]])
4200 else:
4201 print 'Multiple branches match issue %s:' % target_issue
4202 for i in range(len(branches)):
4203 print '%d: %s' % (i, branches[i])
4204 which = raw_input('Choose by index: ')
4205 try:
4206 RunGit(['checkout', branches[int(which)]])
4207 except (IndexError, ValueError):
4208 print 'Invalid selection, not checking out any branch.'
4209 return 1
4210
4211 return 0
4212
4213
maruel@chromium.org29404b52014-09-08 22:58:00 +00004214def CMDlol(parser, args):
4215 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004216 print zlib.decompress(base64.b64decode(
4217 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4218 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4219 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4220 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004221 return 0
4222
4223
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004224class OptionParser(optparse.OptionParser):
4225 """Creates the option parse and add --verbose support."""
4226 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004227 optparse.OptionParser.__init__(
4228 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004229 self.add_option(
4230 '-v', '--verbose', action='count', default=0,
4231 help='Use 2 times for more debugging info')
4232
4233 def parse_args(self, args=None, values=None):
4234 options, args = optparse.OptionParser.parse_args(self, args, values)
4235 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4236 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4237 return options, args
4238
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004239
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004240def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004241 if sys.hexversion < 0x02060000:
4242 print >> sys.stderr, (
4243 '\nYour python version %s is unsupported, please upgrade.\n' %
4244 sys.version.split(' ', 1)[0])
4245 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004246
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004247 # Reload settings.
4248 global settings
4249 settings = Settings()
4250
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004251 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004252 dispatcher = subcommand.CommandDispatcher(__name__)
4253 try:
4254 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004255 except auth.AuthenticationError as e:
4256 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004257 except urllib2.HTTPError, e:
4258 if e.code != 500:
4259 raise
4260 DieWithError(
4261 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4262 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004263 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004264
4265
4266if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004267 # These affect sys.stdout so do it outside of main() to simplify mocks in
4268 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004269 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004270 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004271 try:
4272 sys.exit(main(sys.argv[1:]))
4273 except KeyboardInterrupt:
4274 sys.stderr.write('interrupted\n')
4275 sys.exit(1)