blob: 15f44a99d5859f791c6ba451d4f554b335731ccf [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.orgeba7b7e2016-03-24 17:32:34 +00001596 # TODO(tandrii)
1597 raise NotImplementedError()
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001599
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001600class ChangeDescription(object):
1601 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001602 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001603 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001604
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001605 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001606 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001607
agable@chromium.org42c20792013-09-12 17:34:49 +00001608 @property # www.logilab.org/ticket/89786
1609 def description(self): # pylint: disable=E0202
1610 return '\n'.join(self._description_lines)
1611
1612 def set_description(self, desc):
1613 if isinstance(desc, basestring):
1614 lines = desc.splitlines()
1615 else:
1616 lines = [line.rstrip() for line in desc]
1617 while lines and not lines[0]:
1618 lines.pop(0)
1619 while lines and not lines[-1]:
1620 lines.pop(-1)
1621 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001622
piman@chromium.org336f9122014-09-04 02:16:55 +00001623 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001624 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001625 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001626 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001627 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001628 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001629
agable@chromium.org42c20792013-09-12 17:34:49 +00001630 # Get the set of R= and TBR= lines and remove them from the desciption.
1631 regexp = re.compile(self.R_LINE)
1632 matches = [regexp.match(line) for line in self._description_lines]
1633 new_desc = [l for i, l in enumerate(self._description_lines)
1634 if not matches[i]]
1635 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001636
agable@chromium.org42c20792013-09-12 17:34:49 +00001637 # Construct new unified R= and TBR= lines.
1638 r_names = []
1639 tbr_names = []
1640 for match in matches:
1641 if not match:
1642 continue
1643 people = cleanup_list([match.group(2).strip()])
1644 if match.group(1) == 'TBR':
1645 tbr_names.extend(people)
1646 else:
1647 r_names.extend(people)
1648 for name in r_names:
1649 if name not in reviewers:
1650 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001651 if add_owners_tbr:
1652 owners_db = owners.Database(change.RepositoryRoot(),
1653 fopen=file, os_path=os.path, glob=glob.glob)
1654 all_reviewers = set(tbr_names + reviewers)
1655 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1656 all_reviewers)
1657 tbr_names.extend(owners_db.reviewers_for(missing_files,
1658 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001659 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1660 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1661
1662 # Put the new lines in the description where the old first R= line was.
1663 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1664 if 0 <= line_loc < len(self._description_lines):
1665 if new_tbr_line:
1666 self._description_lines.insert(line_loc, new_tbr_line)
1667 if new_r_line:
1668 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001669 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001670 if new_r_line:
1671 self.append_footer(new_r_line)
1672 if new_tbr_line:
1673 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001674
1675 def prompt(self):
1676 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001677 self.set_description([
1678 '# Enter a description of the change.',
1679 '# This will be displayed on the codereview site.',
1680 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001681 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001682 '--------------------',
1683 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001684
agable@chromium.org42c20792013-09-12 17:34:49 +00001685 regexp = re.compile(self.BUG_LINE)
1686 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001687 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001688 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001689 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001690 if not content:
1691 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001692 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001693
1694 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001695 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1696 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001697 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001698 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001699
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001700 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001701 if self._description_lines:
1702 # Add an empty line if either the last line or the new line isn't a tag.
1703 last_line = self._description_lines[-1]
1704 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1705 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1706 self._description_lines.append('')
1707 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001708
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001709 def get_reviewers(self):
1710 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001711 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1712 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001713 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001714
1715
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001716def get_approving_reviewers(props):
1717 """Retrieves the reviewers that approved a CL from the issue properties with
1718 messages.
1719
1720 Note that the list may contain reviewers that are not committer, thus are not
1721 considered by the CQ.
1722 """
1723 return sorted(
1724 set(
1725 message['sender']
1726 for message in props['messages']
1727 if message['approval'] and message['sender'] in props['reviewers']
1728 )
1729 )
1730
1731
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732def FindCodereviewSettingsFile(filename='codereview.settings'):
1733 """Finds the given file starting in the cwd and going up.
1734
1735 Only looks up to the top of the repository unless an
1736 'inherit-review-settings-ok' file exists in the root of the repository.
1737 """
1738 inherit_ok_file = 'inherit-review-settings-ok'
1739 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001740 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001741 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1742 root = '/'
1743 while True:
1744 if filename in os.listdir(cwd):
1745 if os.path.isfile(os.path.join(cwd, filename)):
1746 return open(os.path.join(cwd, filename))
1747 if cwd == root:
1748 break
1749 cwd = os.path.dirname(cwd)
1750
1751
1752def LoadCodereviewSettingsFromFile(fileobj):
1753 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001754 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 def SetProperty(name, setting, unset_error_ok=False):
1757 fullname = 'rietveld.' + name
1758 if setting in keyvals:
1759 RunGit(['config', fullname, keyvals[setting]])
1760 else:
1761 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1762
1763 SetProperty('server', 'CODE_REVIEW_SERVER')
1764 # Only server setting is required. Other settings can be absent.
1765 # In that case, we ignore errors raised during option deletion attempt.
1766 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001767 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1769 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001770 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001771 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001772 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1773 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001774 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001775 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001776 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001777 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1778 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001780 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001781 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001782
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001783 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1784 RunGit(['config', 'gerrit.squash-uploads',
1785 keyvals['GERRIT_SQUASH_UPLOADS']])
1786
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001787 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1788 #should be of the form
1789 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1790 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1791 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1792 keyvals['ORIGIN_URL_CONFIG']])
1793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001795def urlretrieve(source, destination):
1796 """urllib is broken for SSL connections via a proxy therefore we
1797 can't use urllib.urlretrieve()."""
1798 with open(destination, 'w') as f:
1799 f.write(urllib2.urlopen(source).read())
1800
1801
ukai@chromium.org712d6102013-11-27 00:52:58 +00001802def hasSheBang(fname):
1803 """Checks fname is a #! script."""
1804 with open(fname) as f:
1805 return f.read(2).startswith('#!')
1806
1807
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001808def DownloadGerritHook(force):
1809 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001810
1811 Args:
1812 force: True to update hooks. False to install hooks if not present.
1813 """
1814 if not settings.GetIsGerrit():
1815 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001816 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001817 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1818 if not os.access(dst, os.X_OK):
1819 if os.path.exists(dst):
1820 if not force:
1821 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001822 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001823 print(
1824 'WARNING: installing Gerrit commit-msg hook.\n'
1825 ' This behavior of git cl will soon be disabled.\n'
1826 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001827 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001828 if not hasSheBang(dst):
1829 DieWithError('Not a script: %s\n'
1830 'You need to download from\n%s\n'
1831 'into .git/hooks/commit-msg and '
1832 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001833 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1834 except Exception:
1835 if os.path.exists(dst):
1836 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001837 DieWithError('\nFailed to download hooks.\n'
1838 'You need to download from\n%s\n'
1839 'into .git/hooks/commit-msg and '
1840 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001841
1842
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001843
1844def GetRietveldCodereviewSettingsInteractively():
1845 """Prompt the user for settings."""
1846 server = settings.GetDefaultServerUrl(error_ok=True)
1847 prompt = 'Rietveld server (host[:port])'
1848 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1849 newserver = ask_for_data(prompt + ':')
1850 if not server and not newserver:
1851 newserver = DEFAULT_SERVER
1852 if newserver:
1853 newserver = gclient_utils.UpgradeToHttps(newserver)
1854 if newserver != server:
1855 RunGit(['config', 'rietveld.server', newserver])
1856
1857 def SetProperty(initial, caption, name, is_url):
1858 prompt = caption
1859 if initial:
1860 prompt += ' ("x" to clear) [%s]' % initial
1861 new_val = ask_for_data(prompt + ':')
1862 if new_val == 'x':
1863 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1864 elif new_val:
1865 if is_url:
1866 new_val = gclient_utils.UpgradeToHttps(new_val)
1867 if new_val != initial:
1868 RunGit(['config', 'rietveld.' + name, new_val])
1869
1870 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1871 SetProperty(settings.GetDefaultPrivateFlag(),
1872 'Private flag (rietveld only)', 'private', False)
1873 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1874 'tree-status-url', False)
1875 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1876 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1877 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1878 'run-post-upload-hook', False)
1879
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001880@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001882 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001883
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001884 print('WARNING: git cl config works for Rietveld only.\n'
1885 'For Gerrit, see http://crbug.com/579160.')
1886 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001887 parser.add_option('--activate-update', action='store_true',
1888 help='activate auto-updating [rietveld] section in '
1889 '.git/config')
1890 parser.add_option('--deactivate-update', action='store_true',
1891 help='deactivate auto-updating [rietveld] section in '
1892 '.git/config')
1893 options, args = parser.parse_args(args)
1894
1895 if options.deactivate_update:
1896 RunGit(['config', 'rietveld.autoupdate', 'false'])
1897 return
1898
1899 if options.activate_update:
1900 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1901 return
1902
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001904 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 return 0
1906
1907 url = args[0]
1908 if not url.endswith('codereview.settings'):
1909 url = os.path.join(url, 'codereview.settings')
1910
1911 # Load code review settings and download hooks (if available).
1912 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1913 return 0
1914
1915
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001916def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001917 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001918 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1919 branch = ShortBranchName(branchref)
1920 _, args = parser.parse_args(args)
1921 if not args:
1922 print("Current base-url:")
1923 return RunGit(['config', 'branch.%s.base-url' % branch],
1924 error_ok=False).strip()
1925 else:
1926 print("Setting base-url to %s" % args[0])
1927 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1928 error_ok=False).strip()
1929
1930
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001931def color_for_status(status):
1932 """Maps a Changelist status to color, for CMDstatus and other tools."""
1933 return {
1934 'unsent': Fore.RED,
1935 'waiting': Fore.BLUE,
1936 'reply': Fore.YELLOW,
1937 'lgtm': Fore.GREEN,
1938 'commit': Fore.MAGENTA,
1939 'closed': Fore.CYAN,
1940 'error': Fore.WHITE,
1941 }.get(status, Fore.WHITE)
1942
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001943def fetch_cl_status(branch, auth_config=None):
1944 """Fetches information for an issue and returns (branch, issue, status)."""
1945 cl = Changelist(branchref=branch, auth_config=auth_config)
1946 url = cl.GetIssueURL()
1947 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001948
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001949 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001950 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001951 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001952
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001953 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001954
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001955def get_cl_statuses(
1956 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001957 """Returns a blocking iterable of (branch, issue, color) for given branches.
1958
1959 If fine_grained is true, this will fetch CL statuses from the server.
1960 Otherwise, simply indicate if there's a matching url for the given branches.
1961
1962 If max_processes is specified, it is used as the maximum number of processes
1963 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1964 spawned.
1965 """
1966 # Silence upload.py otherwise it becomes unwieldly.
1967 upload.verbosity = 0
1968
1969 if fine_grained:
1970 # Process one branch synchronously to work through authentication, then
1971 # spawn processes to process all the other branches in parallel.
1972 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001973 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1974 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001975
1976 branches_to_fetch = branches[1:]
1977 pool = ThreadPool(
1978 min(max_processes, len(branches_to_fetch))
1979 if max_processes is not None
1980 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001981 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001982 yield x
1983 else:
1984 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1985 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001986 cl = Changelist(branchref=b, auth_config=auth_config)
1987 url = cl.GetIssueURL()
1988 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001989
rmistry@google.com2dd99862015-06-22 12:22:18 +00001990
1991def upload_branch_deps(cl, args):
1992 """Uploads CLs of local branches that are dependents of the current branch.
1993
1994 If the local branch dependency tree looks like:
1995 test1 -> test2.1 -> test3.1
1996 -> test3.2
1997 -> test2.2 -> test3.3
1998
1999 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2000 run on the dependent branches in this order:
2001 test2.1, test3.1, test3.2, test2.2, test3.3
2002
2003 Note: This function does not rebase your local dependent branches. Use it when
2004 you make a change to the parent branch that will not conflict with its
2005 dependent branches, and you would like their dependencies updated in
2006 Rietveld.
2007 """
2008 if git_common.is_dirty_git_tree('upload-branch-deps'):
2009 return 1
2010
2011 root_branch = cl.GetBranch()
2012 if root_branch is None:
2013 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2014 'Get on a branch!')
2015 if not cl.GetIssue() or not cl.GetPatchset():
2016 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2017 'patchset dependencies without an uploaded CL.')
2018
2019 branches = RunGit(['for-each-ref',
2020 '--format=%(refname:short) %(upstream:short)',
2021 'refs/heads'])
2022 if not branches:
2023 print('No local branches found.')
2024 return 0
2025
2026 # Create a dictionary of all local branches to the branches that are dependent
2027 # on it.
2028 tracked_to_dependents = collections.defaultdict(list)
2029 for b in branches.splitlines():
2030 tokens = b.split()
2031 if len(tokens) == 2:
2032 branch_name, tracked = tokens
2033 tracked_to_dependents[tracked].append(branch_name)
2034
2035 print
2036 print 'The dependent local branches of %s are:' % root_branch
2037 dependents = []
2038 def traverse_dependents_preorder(branch, padding=''):
2039 dependents_to_process = tracked_to_dependents.get(branch, [])
2040 padding += ' '
2041 for dependent in dependents_to_process:
2042 print '%s%s' % (padding, dependent)
2043 dependents.append(dependent)
2044 traverse_dependents_preorder(dependent, padding)
2045 traverse_dependents_preorder(root_branch)
2046 print
2047
2048 if not dependents:
2049 print 'There are no dependent local branches for %s' % root_branch
2050 return 0
2051
2052 print ('This command will checkout all dependent branches and run '
2053 '"git cl upload".')
2054 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2055
andybons@chromium.org962f9462016-02-03 20:00:42 +00002056 # Add a default patchset title to all upload calls in Rietveld.
2057 if not settings.GetIsGerrit():
2058 args.extend(['-t', 'Updated patchset dependency'])
2059
rmistry@google.com2dd99862015-06-22 12:22:18 +00002060 # Record all dependents that failed to upload.
2061 failures = {}
2062 # Go through all dependents, checkout the branch and upload.
2063 try:
2064 for dependent_branch in dependents:
2065 print
2066 print '--------------------------------------'
2067 print 'Running "git cl upload" from %s:' % dependent_branch
2068 RunGit(['checkout', '-q', dependent_branch])
2069 print
2070 try:
2071 if CMDupload(OptionParser(), args) != 0:
2072 print 'Upload failed for %s!' % dependent_branch
2073 failures[dependent_branch] = 1
2074 except: # pylint: disable=W0702
2075 failures[dependent_branch] = 1
2076 print
2077 finally:
2078 # Swap back to the original root branch.
2079 RunGit(['checkout', '-q', root_branch])
2080
2081 print
2082 print 'Upload complete for dependent branches!'
2083 for dependent_branch in dependents:
2084 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2085 print ' %s : %s' % (dependent_branch, upload_status)
2086 print
2087
2088 return 0
2089
2090
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002092 """Show status of changelists.
2093
2094 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002095 - Red not sent for review or broken
2096 - Blue waiting for review
2097 - Yellow waiting for you to reply to review
2098 - Green LGTM'ed
2099 - Magenta in the commit queue
2100 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002101
2102 Also see 'git cl comments'.
2103 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104 parser.add_option('--field',
2105 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002106 parser.add_option('-f', '--fast', action='store_true',
2107 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002108 parser.add_option(
2109 '-j', '--maxjobs', action='store', type=int,
2110 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002111
2112 auth.add_auth_options(parser)
2113 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002114 if args:
2115 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002116 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002119 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 if options.field.startswith('desc'):
2121 print cl.GetDescription()
2122 elif options.field == 'id':
2123 issueid = cl.GetIssue()
2124 if issueid:
2125 print issueid
2126 elif options.field == 'patch':
2127 patchset = cl.GetPatchset()
2128 if patchset:
2129 print patchset
2130 elif options.field == 'url':
2131 url = cl.GetIssueURL()
2132 if url:
2133 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002134 return 0
2135
2136 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2137 if not branches:
2138 print('No local branch found.')
2139 return 0
2140
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002141 changes = (
2142 Changelist(branchref=b, auth_config=auth_config)
2143 for b in branches.splitlines())
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002144 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002145 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002146 alignment = max(5, max(len(b) for b in branches))
2147 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002148 output = get_cl_statuses(branches,
2149 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002150 max_processes=options.maxjobs,
2151 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002152
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002153 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002154 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002155 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002156 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002157 b, i, status = output.next()
2158 branch_statuses[b] = (i, status)
2159 issue_url, status = branch_statuses.pop(branch)
2160 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002161 reset = Fore.RESET
2162 if not sys.stdout.isatty():
2163 color = ''
2164 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002165 status_str = '(%s)' % status if status else ''
2166 print ' %*s : %s%s %s%s' % (
2167 alignment, ShortBranchName(branch), color, issue_url, status_str,
2168 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002169
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002170 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002171 print
2172 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002173 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002174 if not cl.GetIssue():
2175 print 'No issue assigned.'
2176 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002177 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002178 if not options.fast:
2179 print 'Issue description:'
2180 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002181 return 0
2182
2183
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002184def colorize_CMDstatus_doc():
2185 """To be called once in main() to add colors to git cl status help."""
2186 colors = [i for i in dir(Fore) if i[0].isupper()]
2187
2188 def colorize_line(line):
2189 for color in colors:
2190 if color in line.upper():
2191 # Extract whitespaces first and the leading '-'.
2192 indent = len(line) - len(line.lstrip(' ')) + 1
2193 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2194 return line
2195
2196 lines = CMDstatus.__doc__.splitlines()
2197 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2198
2199
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002200@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002202 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203
2204 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002205 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002206 parser.add_option('-r', '--reverse', action='store_true',
2207 help='Lookup the branch(es) for the specified issues. If '
2208 'no issues are specified, all branches with mapped '
2209 'issues will be listed.')
2210 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002211
dnj@chromium.org406c4402015-03-03 17:22:28 +00002212 if options.reverse:
2213 branches = RunGit(['for-each-ref', 'refs/heads',
2214 '--format=%(refname:short)']).splitlines()
2215
2216 # Reverse issue lookup.
2217 issue_branch_map = {}
2218 for branch in branches:
2219 cl = Changelist(branchref=branch)
2220 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2221 if not args:
2222 args = sorted(issue_branch_map.iterkeys())
2223 for issue in args:
2224 if not issue:
2225 continue
2226 print 'Branch for issue number %s: %s' % (
2227 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2228 else:
2229 cl = Changelist()
2230 if len(args) > 0:
2231 try:
2232 issue = int(args[0])
2233 except ValueError:
2234 DieWithError('Pass a number to set the issue or none to list it.\n'
2235 'Maybe you want to run git cl status?')
2236 cl.SetIssue(issue)
2237 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002238 return 0
2239
2240
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002241def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002242 """Shows or posts review comments for any changelist."""
2243 parser.add_option('-a', '--add-comment', dest='comment',
2244 help='comment to add to an issue')
2245 parser.add_option('-i', dest='issue',
2246 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002247 parser.add_option('-j', '--json-file',
2248 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002249 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002250 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002251 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002252
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002253 issue = None
2254 if options.issue:
2255 try:
2256 issue = int(options.issue)
2257 except ValueError:
2258 DieWithError('A review issue id is expected to be a number')
2259
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002260 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002261
2262 if options.comment:
2263 cl.AddComment(options.comment)
2264 return 0
2265
2266 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002267 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002268 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002269 summary.append({
2270 'date': message['date'],
2271 'lgtm': False,
2272 'message': message['text'],
2273 'not_lgtm': False,
2274 'sender': message['sender'],
2275 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002276 if message['disapproval']:
2277 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002278 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002279 elif message['approval']:
2280 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002281 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002282 elif message['sender'] == data['owner_email']:
2283 color = Fore.MAGENTA
2284 else:
2285 color = Fore.BLUE
2286 print '\n%s%s %s%s' % (
2287 color, message['date'].split('.', 1)[0], message['sender'],
2288 Fore.RESET)
2289 if message['text'].strip():
2290 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002291 if options.json_file:
2292 with open(options.json_file, 'wb') as f:
2293 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002294 return 0
2295
2296
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002297def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002298 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002299 parser.add_option('-d', '--display', action='store_true',
2300 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002301 auth.add_auth_options(parser)
2302 options, _ = parser.parse_args(args)
2303 auth_config = auth.extract_auth_config_from_options(options)
2304 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002305 if not cl.GetIssue():
2306 DieWithError('This branch has no associated changelist.')
2307 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002308 if options.display:
2309 print description.description
2310 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002311 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002312 if cl.GetDescription() != description.description:
2313 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002314 return 0
2315
2316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002317def CreateDescriptionFromLog(args):
2318 """Pulls out the commit log to use as a base for the CL description."""
2319 log_args = []
2320 if len(args) == 1 and not args[0].endswith('.'):
2321 log_args = [args[0] + '..']
2322 elif len(args) == 1 and args[0].endswith('...'):
2323 log_args = [args[0][:-1]]
2324 elif len(args) == 2:
2325 log_args = [args[0] + '..' + args[1]]
2326 else:
2327 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002328 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002329
2330
thestig@chromium.org44202a22014-03-11 19:22:18 +00002331def CMDlint(parser, args):
2332 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002333 parser.add_option('--filter', action='append', metavar='-x,+y',
2334 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002335 auth.add_auth_options(parser)
2336 options, args = parser.parse_args(args)
2337 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002338
2339 # Access to a protected member _XX of a client class
2340 # pylint: disable=W0212
2341 try:
2342 import cpplint
2343 import cpplint_chromium
2344 except ImportError:
2345 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2346 return 1
2347
2348 # Change the current working directory before calling lint so that it
2349 # shows the correct base.
2350 previous_cwd = os.getcwd()
2351 os.chdir(settings.GetRoot())
2352 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002353 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002354 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2355 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002356 if not files:
2357 print "Cannot lint an empty CL"
2358 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002359
2360 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002361 command = args + files
2362 if options.filter:
2363 command = ['--filter=' + ','.join(options.filter)] + command
2364 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002365
2366 white_regex = re.compile(settings.GetLintRegex())
2367 black_regex = re.compile(settings.GetLintIgnoreRegex())
2368 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2369 for filename in filenames:
2370 if white_regex.match(filename):
2371 if black_regex.match(filename):
2372 print "Ignoring file %s" % filename
2373 else:
2374 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2375 extra_check_functions)
2376 else:
2377 print "Skipping file %s" % filename
2378 finally:
2379 os.chdir(previous_cwd)
2380 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2381 if cpplint._cpplint_state.error_count != 0:
2382 return 1
2383 return 0
2384
2385
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002386def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002387 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002388 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002389 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002390 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002391 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002392 auth.add_auth_options(parser)
2393 options, args = parser.parse_args(args)
2394 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002395
sbc@chromium.org71437c02015-04-09 19:29:40 +00002396 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002397 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002398 return 1
2399
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002400 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002401 if args:
2402 base_branch = args[0]
2403 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002404 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002405 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002406
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002407 cl.RunHook(
2408 committing=not options.upload,
2409 may_prompt=False,
2410 verbose=options.verbose,
2411 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002412 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002413
2414
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002415def AddChangeIdToCommitMessage(options, args):
2416 """Re-commits using the current message, assumes the commit hook is in
2417 place.
2418 """
2419 log_desc = options.message or CreateDescriptionFromLog(args)
2420 git_command = ['commit', '--amend', '-m', log_desc]
2421 RunGit(git_command)
2422 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002423 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002424 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002425 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002426 else:
2427 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2428
2429
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002430def GenerateGerritChangeId(message):
2431 """Returns Ixxxxxx...xxx change id.
2432
2433 Works the same way as
2434 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2435 but can be called on demand on all platforms.
2436
2437 The basic idea is to generate git hash of a state of the tree, original commit
2438 message, author/committer info and timestamps.
2439 """
2440 lines = []
2441 tree_hash = RunGitSilent(['write-tree'])
2442 lines.append('tree %s' % tree_hash.strip())
2443 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2444 if code == 0:
2445 lines.append('parent %s' % parent.strip())
2446 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2447 lines.append('author %s' % author.strip())
2448 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2449 lines.append('committer %s' % committer.strip())
2450 lines.append('')
2451 # Note: Gerrit's commit-hook actually cleans message of some lines and
2452 # whitespace. This code is not doing this, but it clearly won't decrease
2453 # entropy.
2454 lines.append(message)
2455 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2456 stdin='\n'.join(lines))
2457 return 'I%s' % change_hash.strip()
2458
2459
piman@chromium.org336f9122014-09-04 02:16:55 +00002460def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002461 """upload the current branch to gerrit."""
2462 # We assume the remote called "origin" is the one we want.
2463 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002464 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002465
2466 remote, remote_branch = cl.GetRemoteBranch()
2467 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2468 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002469
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002470 change_desc = ChangeDescription(
2471 options.message or CreateDescriptionFromLog(args))
2472 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002473 print "\nDescription is empty. Aborting..."
2474 return 1
2475
2476 if options.title:
2477 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002478 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002479
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002480 if options.squash:
2481 # Try to get the message from a previous upload.
2482 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002483 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002484 if not message:
2485 if not options.force:
2486 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002487 if not change_desc.description:
2488 print "Description is empty; aborting."
2489 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002490 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002491 change_ids = git_footers.get_footer_change_id(message)
2492 if len(change_ids) > 1:
2493 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2494 if not change_ids:
2495 message = git_footers.add_footer_change_id(
2496 message, GenerateGerritChangeId(message))
2497 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002498 change_ids = git_footers.get_footer_change_id(message)
2499 assert len(change_ids) == 1
2500
2501 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002502
2503 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2504 if remote is '.':
2505 # If our upstream branch is local, we base our squashed commit on its
2506 # squashed version.
2507 parent = ('refs/heads/git_cl_uploads/' +
2508 scm.GIT.ShortBranchName(upstream_branch))
2509
2510 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2511 # will create additional CLs when uploading.
2512 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2513 RunGitSilent(['rev-parse', parent + ':'])):
2514 print 'Upload upstream branch ' + upstream_branch + ' first.'
2515 return 1
2516 else:
2517 parent = cl.GetCommonAncestorWithUpstream()
2518
2519 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2520 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2521 '-m', message]).strip()
2522 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002523 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002524 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002525 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002526 ref_to_push = 'HEAD'
2527 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002528 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002529
2530 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2531 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002532 if len(commits) > 1:
2533 print('WARNING: This will upload %d commits. Run the following command '
2534 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002535 print('git log %s..%s' % (parent, ref_to_push))
2536 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002537 'commit.')
2538 ask_for_data('About to upload; enter to confirm.')
2539
piman@chromium.org336f9122014-09-04 02:16:55 +00002540 if options.reviewers or options.tbr_owners:
2541 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002542
ukai@chromium.orge8077812012-02-03 03:41:46 +00002543 receive_options = []
2544 cc = cl.GetCCList().split(',')
2545 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002546 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002547 cc = filter(None, cc)
2548 if cc:
2549 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002550 if change_desc.get_reviewers():
2551 receive_options.extend(
2552 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002553
ukai@chromium.orge8077812012-02-03 03:41:46 +00002554 git_command = ['push']
2555 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002556 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002557 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002558 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002559 push_stdout = gclient_utils.CheckCallAndFilter(
2560 ['git'] + git_command,
2561 print_stdout=True,
2562 # Flush after every line: useful for seeing progress when running as
2563 # recipe.
2564 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002565
2566 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002567 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2568 change_numbers = [m.group(1)
2569 for m in map(regex.match, push_stdout.splitlines())
2570 if m]
2571 if len(change_numbers) != 1:
2572 DieWithError(
2573 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2574 'Change-Id: %s') % (len(change_numbers), change_id))
2575 cl.SetIssue(change_numbers[0])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002576 head = RunGit(['rev-parse', 'HEAD']).strip()
2577 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002578 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002580
wittman@chromium.org455dc922015-01-26 20:15:50 +00002581def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2582 """Computes the remote branch ref to use for the CL.
2583
2584 Args:
2585 remote (str): The git remote for the CL.
2586 remote_branch (str): The git remote branch for the CL.
2587 target_branch (str): The target branch specified by the user.
2588 pending_prefix (str): The pending prefix from the settings.
2589 """
2590 if not (remote and remote_branch):
2591 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002592
wittman@chromium.org455dc922015-01-26 20:15:50 +00002593 if target_branch:
2594 # Cannonicalize branch references to the equivalent local full symbolic
2595 # refs, which are then translated into the remote full symbolic refs
2596 # below.
2597 if '/' not in target_branch:
2598 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2599 else:
2600 prefix_replacements = (
2601 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2602 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2603 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2604 )
2605 match = None
2606 for regex, replacement in prefix_replacements:
2607 match = re.search(regex, target_branch)
2608 if match:
2609 remote_branch = target_branch.replace(match.group(0), replacement)
2610 break
2611 if not match:
2612 # This is a branch path but not one we recognize; use as-is.
2613 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002614 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2615 # Handle the refs that need to land in different refs.
2616 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002617
wittman@chromium.org455dc922015-01-26 20:15:50 +00002618 # Create the true path to the remote branch.
2619 # Does the following translation:
2620 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2621 # * refs/remotes/origin/master -> refs/heads/master
2622 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2623 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2624 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2625 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2626 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2627 'refs/heads/')
2628 elif remote_branch.startswith('refs/remotes/branch-heads'):
2629 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2630 # If a pending prefix exists then replace refs/ with it.
2631 if pending_prefix:
2632 remote_branch = remote_branch.replace('refs/', pending_prefix)
2633 return remote_branch
2634
2635
piman@chromium.org336f9122014-09-04 02:16:55 +00002636def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002637 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002638 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002639 upload_args.extend(['--server', cl.GetCodereviewServer()])
2640 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
2641 upload_args.extend(auth.auth_config_to_command_options(
2642 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002643 if options.emulate_svn_auto_props:
2644 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002645
2646 change_desc = None
2647
pgervais@chromium.org91141372014-01-09 23:27:20 +00002648 if options.email is not None:
2649 upload_args.extend(['--email', options.email])
2650
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002651 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002652 if options.title:
2653 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002654 if options.message:
2655 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002656 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002657 print ("This branch is associated with issue %s. "
2658 "Adding patch to that issue." % cl.GetIssue())
2659 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002660 if options.title:
2661 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002662 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002663 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002664 if options.reviewers or options.tbr_owners:
2665 change_desc.update_reviewers(options.reviewers,
2666 options.tbr_owners,
2667 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002668 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002669 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002670
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002671 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002672 print "Description is empty; aborting."
2673 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002674
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002675 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002676 if change_desc.get_reviewers():
2677 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002678 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002679 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002680 DieWithError("Must specify reviewers to send email.")
2681 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002682
2683 # We check this before applying rietveld.private assuming that in
2684 # rietveld.cc only addresses which we can send private CLs to are listed
2685 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2686 # --private is specified explicitly on the command line.
2687 if options.private:
2688 logging.warn('rietveld.cc is ignored since private flag is specified. '
2689 'You need to review and add them manually if necessary.')
2690 cc = cl.GetCCListWithoutDefault()
2691 else:
2692 cc = cl.GetCCList()
2693 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002694 if cc:
2695 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002696
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002697 if options.private or settings.GetDefaultPrivateFlag() == "True":
2698 upload_args.append('--private')
2699
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002700 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002701 if not options.find_copies:
2702 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002704 # Include the upstream repo's URL in the change -- this is useful for
2705 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002706 remote_url = cl.GetGitBaseUrlFromConfig()
2707 if not remote_url:
2708 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002709 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002710 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002711 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2712 remote_url = (cl.GetRemoteUrl() + '@'
2713 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714 if remote_url:
2715 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002716 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002717 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2718 settings.GetPendingRefPrefix())
2719 if target_ref:
2720 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002721
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002722 # Look for dependent patchsets. See crbug.com/480453 for more details.
2723 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2724 upstream_branch = ShortBranchName(upstream_branch)
2725 if remote is '.':
2726 # A local branch is being tracked.
2727 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002728 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002729 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002730 print ('Skipping dependency patchset upload because git config '
2731 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002732 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002733 else:
2734 auth_config = auth.extract_auth_config_from_options(options)
2735 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2736 branch_cl_issue_url = branch_cl.GetIssueURL()
2737 branch_cl_issue = branch_cl.GetIssue()
2738 branch_cl_patchset = branch_cl.GetPatchset()
2739 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2740 upload_args.extend(
2741 ['--depends_on_patchset', '%s:%s' % (
2742 branch_cl_issue, branch_cl_patchset)])
2743 print
2744 print ('The current branch (%s) is tracking a local branch (%s) with '
2745 'an associated CL.') % (cl.GetBranch(), local_branch)
2746 print 'Adding %s/#ps%s as a dependency patchset.' % (
2747 branch_cl_issue_url, branch_cl_patchset)
2748 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002749
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002750 project = settings.GetProject()
2751 if project:
2752 upload_args.extend(['--project', project])
2753
rmistry@google.comef966222015-04-07 11:15:01 +00002754 if options.cq_dry_run:
2755 upload_args.extend(['--cq_dry_run'])
2756
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002757 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002758 upload_args = ['upload'] + upload_args + args
2759 logging.info('upload.RealMain(%s)', upload_args)
2760 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002761 issue = int(issue)
2762 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002763 except KeyboardInterrupt:
2764 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002765 except:
2766 # If we got an exception after the user typed a description for their
2767 # change, back up the description before re-raising.
2768 if change_desc:
2769 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2770 print '\nGot exception while uploading -- saving description to %s\n' \
2771 % backup_path
2772 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002773 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002774 backup_file.close()
2775 raise
2776
2777 if not cl.GetIssue():
2778 cl.SetIssue(issue)
2779 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002780
2781 if options.use_commit_queue:
2782 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002783 return 0
2784
2785
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002786def cleanup_list(l):
2787 """Fixes a list so that comma separated items are put as individual items.
2788
2789 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2790 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2791 """
2792 items = sum((i.split(',') for i in l), [])
2793 stripped_items = (i.strip() for i in items)
2794 return sorted(filter(None, stripped_items))
2795
2796
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002797@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002798def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002799 """Uploads the current changelist to codereview.
2800
2801 Can skip dependency patchset uploads for a branch by running:
2802 git config branch.branch_name.skip-deps-uploads True
2803 To unset run:
2804 git config --unset branch.branch_name.skip-deps-uploads
2805 Can also set the above globally by using the --global flag.
2806 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002807 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2808 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002809 parser.add_option('--bypass-watchlists', action='store_true',
2810 dest='bypass_watchlists',
2811 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002812 parser.add_option('-f', action='store_true', dest='force',
2813 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002814 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002815 parser.add_option('-t', dest='title',
2816 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002817 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002818 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002819 help='reviewer email addresses')
2820 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002821 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002822 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002823 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002824 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002825 parser.add_option('--emulate_svn_auto_props',
2826 '--emulate-svn-auto-props',
2827 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002828 dest="emulate_svn_auto_props",
2829 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002830 parser.add_option('-c', '--use-commit-queue', action='store_true',
2831 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002832 parser.add_option('--private', action='store_true',
2833 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002834 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002835 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002836 metavar='TARGET',
2837 help='Apply CL to remote ref TARGET. ' +
2838 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002839 parser.add_option('--squash', action='store_true',
2840 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002841 parser.add_option('--no-squash', action='store_true',
2842 help='Don\'t squash multiple commits into one ' +
2843 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002844 parser.add_option('--email', default=None,
2845 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002846 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2847 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002848 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2849 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002850 help='Send the patchset to do a CQ dry run right after '
2851 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002852 parser.add_option('--dependencies', action='store_true',
2853 help='Uploads CLs of all the local branches that depend on '
2854 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002855
rmistry@google.com2dd99862015-06-22 12:22:18 +00002856 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002857 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002858 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002859 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002860 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002861
sbc@chromium.org71437c02015-04-09 19:29:40 +00002862 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002863 return 1
2864
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002865 options.reviewers = cleanup_list(options.reviewers)
2866 options.cc = cleanup_list(options.cc)
2867
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002868 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002869 if args:
2870 # TODO(ukai): is it ok for gerrit case?
2871 base_branch = args[0]
2872 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002873 if cl.GetBranch() is None:
2874 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2875
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002876 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002877 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002878 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002879
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002880 # Make sure authenticated to Rietveld before running expensive hooks. It is
2881 # a fast, best efforts check. Rietveld still can reject the authentication
2882 # during the actual upload.
2883 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2884 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002885 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002886 if not authenticator.has_cached_credentials():
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002887 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002888
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002889 # Apply watchlists on upload.
2890 change = cl.GetChange(base_branch, None)
2891 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2892 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002893 if not options.bypass_watchlists:
2894 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002895
ukai@chromium.orge8077812012-02-03 03:41:46 +00002896 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002897 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002898 # Set the reviewer list now so that presubmit checks can access it.
2899 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002900 change_description.update_reviewers(options.reviewers,
2901 options.tbr_owners,
2902 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002903 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002904 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002905 may_prompt=not options.force,
2906 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002907 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002908 if not hook_results.should_continue():
2909 return 1
2910 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002911 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002912
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002913 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002914 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002915 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002916 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002917 print ('The last upload made from this repository was patchset #%d but '
2918 'the most recent patchset on the server is #%d.'
2919 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002920 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2921 'from another machine or branch the patch you\'re uploading now '
2922 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002923 ask_for_data('About to upload; enter to confirm.')
2924
iannucci@chromium.org79540052012-10-19 23:15:26 +00002925 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002926 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002927 if options.squash and options.no_squash:
2928 DieWithError('Can only use one of --squash or --no-squash')
2929
2930 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2931 not options.no_squash)
2932
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002933 ret = GerritUpload(options, args, cl, change)
2934 else:
2935 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002936 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002937 git_set_branch_value('last-upload-hash',
2938 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002939 # Run post upload hooks, if specified.
2940 if settings.GetRunPostUploadHook():
2941 presubmit_support.DoPostUploadExecuter(
2942 change,
2943 cl,
2944 settings.GetRoot(),
2945 options.verbose,
2946 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002947
rmistry@google.com2dd99862015-06-22 12:22:18 +00002948 # Upload all dependencies if specified.
2949 if options.dependencies:
2950 print
2951 print '--dependencies has been specified.'
2952 print 'All dependent local branches will be re-uploaded.'
2953 print
2954 # Remove the dependencies flag from args so that we do not end up in a
2955 # loop.
2956 orig_args.remove('--dependencies')
2957 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002958 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002959
2960
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002961def IsSubmoduleMergeCommit(ref):
2962 # When submodules are added to the repo, we expect there to be a single
2963 # non-git-svn merge commit at remote HEAD with a signature comment.
2964 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002965 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002966 return RunGit(cmd) != ''
2967
2968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002969def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002970 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002971
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002972 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973 Updates changelog with metadata (e.g. pointer to review).
2974 Pushes/dcommits the code upstream.
2975 Updates review and closes.
2976 """
2977 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2978 help='bypass upload presubmit hook')
2979 parser.add_option('-m', dest='message',
2980 help="override review description")
2981 parser.add_option('-f', action='store_true', dest='force',
2982 help="force yes to questions (don't prompt)")
2983 parser.add_option('-c', dest='contributor',
2984 help="external contributor for patch (appended to " +
2985 "description and used as author for git). Should be " +
2986 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002987 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002988 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002989 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002990 auth_config = auth.extract_auth_config_from_options(options)
2991
2992 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002993
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002994 current = cl.GetBranch()
2995 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2996 if not settings.GetIsGitSvn() and remote == '.':
2997 print
2998 print 'Attempting to push branch %r into another local branch!' % current
2999 print
3000 print 'Either reparent this branch on top of origin/master:'
3001 print ' git reparent-branch --root'
3002 print
3003 print 'OR run `git rebase-update` if you think the parent branch is already'
3004 print 'committed.'
3005 print
3006 print ' Current parent: %r' % upstream_branch
3007 return 1
3008
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003009 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003010 # Default to merging against our best guess of the upstream branch.
3011 args = [cl.GetUpstreamBranch()]
3012
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003013 if options.contributor:
3014 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3015 print "Please provide contibutor as 'First Last <email@example.com>'"
3016 return 1
3017
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003018 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003019 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003020
sbc@chromium.org71437c02015-04-09 19:29:40 +00003021 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003022 return 1
3023
3024 # This rev-list syntax means "show all commits not in my branch that
3025 # are in base_branch".
3026 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3027 base_branch]).splitlines()
3028 if upstream_commits:
3029 print ('Base branch "%s" has %d commits '
3030 'not in this branch.' % (base_branch, len(upstream_commits)))
3031 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3032 return 1
3033
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003034 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003035 svn_head = None
3036 if cmd == 'dcommit' or base_has_submodules:
3037 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3038 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003040 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003041 # If the base_head is a submodule merge commit, the first parent of the
3042 # base_head should be a git-svn commit, which is what we're interested in.
3043 base_svn_head = base_branch
3044 if base_has_submodules:
3045 base_svn_head += '^1'
3046
3047 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003048 if extra_commits:
3049 print ('This branch has %d additional commits not upstreamed yet.'
3050 % len(extra_commits.splitlines()))
3051 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3052 'before attempting to %s.' % (base_branch, cmd))
3053 return 1
3054
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003055 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003056 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003057 author = None
3058 if options.contributor:
3059 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003060 hook_results = cl.RunHook(
3061 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003062 may_prompt=not options.force,
3063 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003064 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003065 if not hook_results.should_continue():
3066 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003067
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003068 # Check the tree status if the tree status URL is set.
3069 status = GetTreeStatus()
3070 if 'closed' == status:
3071 print('The tree is closed. Please wait for it to reopen. Use '
3072 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3073 return 1
3074 elif 'unknown' == status:
3075 print('Unable to determine tree status. Please verify manually and '
3076 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3077 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003078
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003079 change_desc = ChangeDescription(options.message)
3080 if not change_desc.description and cl.GetIssue():
3081 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003082
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003083 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003084 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003085 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003086 else:
3087 print 'No description set.'
3088 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3089 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003091 # Keep a separate copy for the commit message, because the commit message
3092 # contains the link to the Rietveld issue, while the Rietveld message contains
3093 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003094 # Keep a separate copy for the commit message.
3095 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003096 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003097
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003098 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003099 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003100 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003101 # after it. Add a period on a new line to circumvent this. Also add a space
3102 # before the period to make sure that Gitiles continues to correctly resolve
3103 # the URL.
3104 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003105 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003106 commit_desc.append_footer('Patch from %s.' % options.contributor)
3107
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003108 print('Description:')
3109 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003110
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003111 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003112 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003113 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003114
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003115 # We want to squash all this branch's commits into one commit with the proper
3116 # description. We do this by doing a "reset --soft" to the base branch (which
3117 # keeps the working copy the same), then dcommitting that. If origin/master
3118 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3119 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003120 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003121 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3122 # Delete the branches if they exist.
3123 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3124 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3125 result = RunGitWithCode(showref_cmd)
3126 if result[0] == 0:
3127 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003128
3129 # We might be in a directory that's present in this branch but not in the
3130 # trunk. Move up to the top of the tree so that git commands that expect a
3131 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003132 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003133 if rel_base_path:
3134 os.chdir(rel_base_path)
3135
3136 # Stuff our change into the merge branch.
3137 # We wrap in a try...finally block so if anything goes wrong,
3138 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003139 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003140 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003141 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003142 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003143 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003144 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003145 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003146 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003147 RunGit(
3148 [
3149 'commit', '--author', options.contributor,
3150 '-m', commit_desc.description,
3151 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003152 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003153 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003154 if base_has_submodules:
3155 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3156 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3157 RunGit(['checkout', CHERRY_PICK_BRANCH])
3158 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003159 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003160 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003161 mirror = settings.GetGitMirror(remote)
3162 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003163 pending_prefix = settings.GetPendingRefPrefix()
3164 if not pending_prefix or branch.startswith(pending_prefix):
3165 # If not using refs/pending/heads/* at all, or target ref is already set
3166 # to pending, then push to the target ref directly.
3167 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003168 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003169 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003170 else:
3171 # Cherry-pick the change on top of pending ref and then push it.
3172 assert branch.startswith('refs/'), branch
3173 assert pending_prefix[-1] == '/', pending_prefix
3174 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003175 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003176 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003177 if retcode == 0:
3178 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003179 else:
3180 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003181 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003182 'svn', 'dcommit',
3183 '-C%s' % options.similarity,
3184 '--no-rebase', '--rmdir',
3185 ]
3186 if settings.GetForceHttpsCommitUrl():
3187 # Allow forcing https commit URLs for some projects that don't allow
3188 # committing to http URLs (like Google Code).
3189 remote_url = cl.GetGitSvnRemoteUrl()
3190 if urlparse.urlparse(remote_url).scheme == 'http':
3191 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003192 cmd_args.append('--commit-url=%s' % remote_url)
3193 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003194 if 'Committed r' in output:
3195 revision = re.match(
3196 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3197 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003198 finally:
3199 # And then swap back to the original branch and clean up.
3200 RunGit(['checkout', '-q', cl.GetBranch()])
3201 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003202 if base_has_submodules:
3203 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003204
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003205 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003206 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003207 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003208
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003209 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003210 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003211 try:
3212 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3213 # We set pushed_to_pending to False, since it made it all the way to the
3214 # real ref.
3215 pushed_to_pending = False
3216 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003217 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003218
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003220 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003222 if not to_pending:
3223 if viewvc_url and revision:
3224 change_desc.append_footer(
3225 'Committed: %s%s' % (viewvc_url, revision))
3226 elif revision:
3227 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228 print ('Closing issue '
3229 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003230 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003232 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003233 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003234 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003235 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003236 if options.bypass_hooks:
3237 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3238 else:
3239 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003240 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003241 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003242
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003243 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003244 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3245 print 'The commit is in the pending queue (%s).' % pending_ref
3246 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003247 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003248 'footer.' % branch)
3249
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003250 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3251 if os.path.isfile(hook):
3252 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003253
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003254 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003255
3256
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003257def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3258 print
3259 print 'Waiting for commit to be landed on %s...' % real_ref
3260 print '(If you are impatient, you may Ctrl-C once without harm)'
3261 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3262 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003263 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003264
3265 loop = 0
3266 while True:
3267 sys.stdout.write('fetching (%d)... \r' % loop)
3268 sys.stdout.flush()
3269 loop += 1
3270
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003271 if mirror:
3272 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003273 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3274 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3275 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3276 for commit in commits.splitlines():
3277 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3278 print 'Found commit on %s' % real_ref
3279 return commit
3280
3281 current_rev = to_rev
3282
3283
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003284def PushToGitPending(remote, pending_ref, upstream_ref):
3285 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3286
3287 Returns:
3288 (retcode of last operation, output log of last operation).
3289 """
3290 assert pending_ref.startswith('refs/'), pending_ref
3291 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3292 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3293 code = 0
3294 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003295 max_attempts = 3
3296 attempts_left = max_attempts
3297 while attempts_left:
3298 if attempts_left != max_attempts:
3299 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3300 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003301
3302 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003303 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003304 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003305 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003306 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003307 print 'Fetch failed with exit code %d.' % code
3308 if out.strip():
3309 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003310 continue
3311
3312 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003313 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003314 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003315 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003316 if code:
3317 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003318 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3319 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003320 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3321 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003322 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003323 return code, out
3324
3325 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003326 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003327 code, out = RunGitWithCode(
3328 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3329 if code == 0:
3330 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003331 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003332 return code, out
3333
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003334 print 'Push failed with exit code %d.' % code
3335 if out.strip():
3336 print out.strip()
3337 if IsFatalPushFailure(out):
3338 print (
3339 'Fatal push error. Make sure your .netrc credentials and git '
3340 'user.email are correct and you have push access to the repo.')
3341 return code, out
3342
3343 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003344 return code, out
3345
3346
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003347def IsFatalPushFailure(push_stdout):
3348 """True if retrying push won't help."""
3349 return '(prohibited by Gerrit)' in push_stdout
3350
3351
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003352@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003353def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003354 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003355 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003356 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003357 # If it looks like previous commits were mirrored with git-svn.
3358 message = """This repository appears to be a git-svn mirror, but no
3359upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3360 else:
3361 message = """This doesn't appear to be an SVN repository.
3362If your project has a true, writeable git repository, you probably want to run
3363'git cl land' instead.
3364If your project has a git mirror of an upstream SVN master, you probably need
3365to run 'git svn init'.
3366
3367Using the wrong command might cause your commit to appear to succeed, and the
3368review to be closed, without actually landing upstream. If you choose to
3369proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003370 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003371 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003372 return SendUpstream(parser, args, 'dcommit')
3373
3374
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003375@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003376def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003377 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003378 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379 print('This appears to be an SVN repository.')
3380 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003381 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003382 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003383 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003384
3385
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003386def ParseIssueNum(arg):
3387 """Parses the issue number from args if present otherwise returns None."""
3388 if re.match(r'\d+', arg):
3389 return arg
3390 if arg.startswith('http'):
3391 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3392 return None
3393
3394
3395@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003396def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003397 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398 parser.add_option('-b', dest='newbranch',
3399 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003400 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003401 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003402 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3403 help='Change to the directory DIR immediately, '
3404 'before doing anything else.')
3405 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003406 help='failed patches spew .rej files rather than '
3407 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3409 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003410
3411 group = optparse.OptionGroup(parser,
3412 """Options for continuing work on the current issue uploaded
3413from a different clone (e.g. different machine). Must be used independently from
3414the other options. No issue number should be specified, and the branch must have
3415an issue number associated with it""")
3416 group.add_option('--reapply', action='store_true',
3417 dest='reapply',
3418 help="""Reset the branch and reapply the issue.
3419CAUTION: This will undo any local changes in this branch""")
3420
3421 group.add_option('--pull', action='store_true', dest='pull',
3422 help="Performs a pull before reapplying.")
3423 parser.add_option_group(group)
3424
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003425 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003426 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003427 auth_config = auth.extract_auth_config_from_options(options)
3428
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003429 issue_arg = None
3430 if options.reapply :
3431 if len(args) > 0:
3432 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003433
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003434 cl = Changelist()
3435 issue_arg = cl.GetIssue()
3436 upstream = cl.GetUpstreamBranch()
3437 if upstream == None:
3438 parser.error("No upstream branch specified. Cannot reset branch")
3439
3440 RunGit(['reset', '--hard', upstream])
3441 if options.pull:
3442 RunGit(['pull'])
3443 else:
3444 if len(args) != 1:
3445 parser.error("Must specify issue number")
3446
3447 issue_arg = ParseIssueNum(args[0])
3448
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003449 # The patch URL works because ParseIssueNum won't do any substitution
3450 # as the re.sub pattern fails to match and just returns it.
3451 if issue_arg == None:
3452 parser.print_help()
3453 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003454
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003455 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003456 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003457 return 1
3458
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003459 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003460 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003461
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003462 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003463 if options.reapply:
3464 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003465 if options.force:
3466 RunGit(['branch', '-D', options.newbranch],
3467 stderr=subprocess2.PIPE, error_ok=True)
3468 RunGit(['checkout', '-b', options.newbranch,
3469 Changelist().GetUpstreamBranch()])
3470
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003471 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003472 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003473
3474
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003475def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003476 # PatchIssue should never be called with a dirty tree. It is up to the
3477 # caller to check this, but just in case we assert here since the
3478 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003479 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003480
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003481 # TODO(tandrii): implement for Gerrit.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003482 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003483 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003484 issue = int(issue_arg)
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003485 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003486 patchset = cl.GetMostRecentPatchset()
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003487 patch_data = cl._codereview_impl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003488 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003489 # Assume it's a URL to the patch. Default to https.
3490 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003491 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003492 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003493 DieWithError('Must pass an issue ID or full URL for '
3494 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003495 issue = int(match.group(2))
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003496 cl = Changelist(issue=issue, codereview='rietveld',
3497 rietvled_server=match.group(1), auth_config=auth_config)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003498 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003499 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003501 # Switch up to the top-level directory, if necessary, in preparation for
3502 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003503 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003504 if top:
3505 os.chdir(top)
3506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003507 # Git patches have a/ at the beginning of source paths. We strip that out
3508 # with a sed script rather than the -p flag to patch so we can feed either
3509 # Git or svn-style patches into the same apply command.
3510 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003511 try:
3512 patch_data = subprocess2.check_output(
3513 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3514 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003515 DieWithError('Git patch mungling failed.')
3516 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003517
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003518 # We use "git apply" to apply the patch instead of "patch" so that we can
3519 # pick up file adds.
3520 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003521 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003522 if directory:
3523 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003524 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003526 elif IsGitVersionAtLeast('1.7.12'):
3527 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003528 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003529 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003530 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003531 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003532 print 'Failed to apply the patch'
3533 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003534
3535 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003536 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003537 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3538 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003539 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3540 % {'i': issue, 'p': patchset})])
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003541 cl = Changelist(codereview='rietveld', auth_config=auth_config,
3542 rietveld_server=cl.GetCodereviewServer())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003543 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003544 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003545 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003546 else:
3547 print "Patch applied to index."
3548 return 0
3549
3550
3551def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003552 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003553 # Provide a wrapper for git svn rebase to help avoid accidental
3554 # git svn dcommit.
3555 # It's the only command that doesn't use parser at all since we just defer
3556 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003557
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003558 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003559
3560
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003561def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003562 """Fetches the tree status and returns either 'open', 'closed',
3563 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003564 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003565 if url:
3566 status = urllib2.urlopen(url).read().lower()
3567 if status.find('closed') != -1 or status == '0':
3568 return 'closed'
3569 elif status.find('open') != -1 or status == '1':
3570 return 'open'
3571 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003572 return 'unset'
3573
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003575def GetTreeStatusReason():
3576 """Fetches the tree status from a json url and returns the message
3577 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003578 url = settings.GetTreeStatusUrl()
3579 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003580 connection = urllib2.urlopen(json_url)
3581 status = json.loads(connection.read())
3582 connection.close()
3583 return status['message']
3584
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003585
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003586def GetBuilderMaster(bot_list):
3587 """For a given builder, fetch the master from AE if available."""
3588 map_url = 'https://builders-map.appspot.com/'
3589 try:
3590 master_map = json.load(urllib2.urlopen(map_url))
3591 except urllib2.URLError as e:
3592 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3593 (map_url, e))
3594 except ValueError as e:
3595 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3596 if not master_map:
3597 return None, 'Failed to build master map.'
3598
3599 result_master = ''
3600 for bot in bot_list:
3601 builder = bot.split(':', 1)[0]
3602 master_list = master_map.get(builder, [])
3603 if not master_list:
3604 return None, ('No matching master for builder %s.' % builder)
3605 elif len(master_list) > 1:
3606 return None, ('The builder name %s exists in multiple masters %s.' %
3607 (builder, master_list))
3608 else:
3609 cur_master = master_list[0]
3610 if not result_master:
3611 result_master = cur_master
3612 elif result_master != cur_master:
3613 return None, 'The builders do not belong to the same master.'
3614 return result_master, None
3615
3616
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003617def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003618 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003619 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003620 status = GetTreeStatus()
3621 if 'unset' == status:
3622 print 'You must configure your tree status URL by running "git cl config".'
3623 return 2
3624
3625 print "The tree is %s" % status
3626 print
3627 print GetTreeStatusReason()
3628 if status != 'open':
3629 return 1
3630 return 0
3631
3632
maruel@chromium.org15192402012-09-06 12:38:29 +00003633def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003634 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003635 group = optparse.OptionGroup(parser, "Try job options")
3636 group.add_option(
3637 "-b", "--bot", action="append",
3638 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3639 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003640 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003641 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003642 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003643 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003644 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003645 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003646 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003647 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003648 "-r", "--revision",
3649 help="Revision to use for the try job; default: the "
3650 "revision will be determined by the try server; see "
3651 "its waterfall for more info")
3652 group.add_option(
3653 "-c", "--clobber", action="store_true", default=False,
3654 help="Force a clobber before building; e.g. don't do an "
3655 "incremental build")
3656 group.add_option(
3657 "--project",
3658 help="Override which project to use. Projects are defined "
3659 "server-side to define what default bot set to use")
3660 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003661 "-p", "--property", dest="properties", action="append", default=[],
3662 help="Specify generic properties in the form -p key1=value1 -p "
3663 "key2=value2 etc (buildbucket only). The value will be treated as "
3664 "json if decodable, or as string otherwise.")
3665 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003666 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003667 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003668 "--use-rietveld", action="store_true", default=False,
3669 help="Use Rietveld to trigger try jobs.")
3670 group.add_option(
3671 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3672 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003673 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003674 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003675 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003676 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003677
machenbach@chromium.org45453142015-09-15 08:45:22 +00003678 if options.use_rietveld and options.properties:
3679 parser.error('Properties can only be specified with buildbucket')
3680
3681 # Make sure that all properties are prop=value pairs.
3682 bad_params = [x for x in options.properties if '=' not in x]
3683 if bad_params:
3684 parser.error('Got properties with missing "=": %s' % bad_params)
3685
maruel@chromium.org15192402012-09-06 12:38:29 +00003686 if args:
3687 parser.error('Unknown arguments: %s' % args)
3688
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003689 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003690 if not cl.GetIssue():
3691 parser.error('Need to upload first')
3692
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003693 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003694 if props.get('closed'):
3695 parser.error('Cannot send tryjobs for a closed CL')
3696
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003697 if props.get('private'):
3698 parser.error('Cannot use trybots with private issue')
3699
maruel@chromium.org15192402012-09-06 12:38:29 +00003700 if not options.name:
3701 options.name = cl.GetBranch()
3702
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003703 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003704 options.master, err_msg = GetBuilderMaster(options.bot)
3705 if err_msg:
3706 parser.error('Tryserver master cannot be found because: %s\n'
3707 'Please manually specify the tryserver master'
3708 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003709
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003710 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003711 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003712 if not options.bot:
3713 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003714
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003715 # Get try masters from PRESUBMIT.py files.
3716 masters = presubmit_support.DoGetTryMasters(
3717 change,
3718 change.LocalPaths(),
3719 settings.GetRoot(),
3720 None,
3721 None,
3722 options.verbose,
3723 sys.stdout)
3724 if masters:
3725 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003726
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003727 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3728 options.bot = presubmit_support.DoGetTrySlaves(
3729 change,
3730 change.LocalPaths(),
3731 settings.GetRoot(),
3732 None,
3733 None,
3734 options.verbose,
3735 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003736
3737 if not options.bot:
3738 # Get try masters from cq.cfg if any.
3739 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3740 # location.
3741 cq_cfg = os.path.join(change.RepositoryRoot(),
3742 'infra', 'config', 'cq.cfg')
3743 if os.path.exists(cq_cfg):
3744 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003745 cq_masters = commit_queue.get_master_builder_map(
3746 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003747 for master, builders in cq_masters.iteritems():
3748 for builder in builders:
3749 # Skip presubmit builders, because these will fail without LGTM.
3750 if 'presubmit' not in builder.lower():
3751 masters.setdefault(master, {})[builder] = ['defaulttests']
3752 if masters:
3753 return masters
3754
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003755 if not options.bot:
3756 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003757
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003758 builders_and_tests = {}
3759 # TODO(machenbach): The old style command-line options don't support
3760 # multiple try masters yet.
3761 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3762 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3763
3764 for bot in old_style:
3765 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003766 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003767 elif ',' in bot:
3768 parser.error('Specify one bot per --bot flag')
3769 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003770 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003771
3772 for bot, tests in new_style:
3773 builders_and_tests.setdefault(bot, []).extend(tests)
3774
3775 # Return a master map with one master to be backwards compatible. The
3776 # master name defaults to an empty string, which will cause the master
3777 # not to be set on rietveld (deprecated).
3778 return {options.master: builders_and_tests}
3779
3780 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003781
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003782 for builders in masters.itervalues():
3783 if any('triggered' in b for b in builders):
3784 print >> sys.stderr, (
3785 'ERROR You are trying to send a job to a triggered bot. This type of'
3786 ' bot requires an\ninitial job from a parent (usually a builder). '
3787 'Instead send your job to the parent.\n'
3788 'Bot list: %s' % builders)
3789 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003790
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003791 patchset = cl.GetMostRecentPatchset()
3792 if patchset and patchset != cl.GetPatchset():
3793 print(
3794 '\nWARNING Mismatch between local config and server. Did a previous '
3795 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3796 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003797 if options.luci:
3798 trigger_luci_job(cl, masters, options)
3799 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003800 try:
3801 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3802 except BuildbucketResponseException as ex:
3803 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003804 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003805 except Exception as e:
3806 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3807 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3808 e, stacktrace)
3809 return 1
3810 else:
3811 try:
3812 cl.RpcServer().trigger_distributed_try_jobs(
3813 cl.GetIssue(), patchset, options.name, options.clobber,
3814 options.revision, masters)
3815 except urllib2.HTTPError as e:
3816 if e.code == 404:
3817 print('404 from rietveld; '
3818 'did you mean to use "git try" instead of "git cl try"?')
3819 return 1
3820 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003821
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003822 for (master, builders) in sorted(masters.iteritems()):
3823 if master:
3824 print 'Master: %s' % master
3825 length = max(len(builder) for builder in builders)
3826 for builder in sorted(builders):
3827 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003828 return 0
3829
3830
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003831def CMDtry_results(parser, args):
3832 group = optparse.OptionGroup(parser, "Try job results options")
3833 group.add_option(
3834 "-p", "--patchset", type=int, help="patchset number if not current.")
3835 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003836 "--print-master", action='store_true', help="print master name as well.")
3837 group.add_option(
3838 "--color", action='store_true', default=sys.stdout.isatty(),
3839 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003840 group.add_option(
3841 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3842 help="Host of buildbucket. The default host is %default.")
3843 parser.add_option_group(group)
3844 auth.add_auth_options(parser)
3845 options, args = parser.parse_args(args)
3846 if args:
3847 parser.error('Unrecognized args: %s' % ' '.join(args))
3848
3849 auth_config = auth.extract_auth_config_from_options(options)
3850 cl = Changelist(auth_config=auth_config)
3851 if not cl.GetIssue():
3852 parser.error('Need to upload first')
3853
3854 if not options.patchset:
3855 options.patchset = cl.GetMostRecentPatchset()
3856 if options.patchset and options.patchset != cl.GetPatchset():
3857 print(
3858 '\nWARNING Mismatch between local config and server. Did a previous '
3859 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3860 'Continuing using\npatchset %s.\n' % options.patchset)
3861 try:
3862 jobs = fetch_try_jobs(auth_config, cl, options)
3863 except BuildbucketResponseException as ex:
3864 print 'Buildbucket error: %s' % ex
3865 return 1
3866 except Exception as e:
3867 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3868 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3869 e, stacktrace)
3870 return 1
3871 print_tryjobs(options, jobs)
3872 return 0
3873
3874
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003875@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003877 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003878 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003879 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003880 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003883 if args:
3884 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003885 branch = cl.GetBranch()
3886 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003887 cl = Changelist()
3888 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003889
3890 # Clear configured merge-base, if there is one.
3891 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003892 else:
3893 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003894 return 0
3895
3896
thestig@chromium.org00858c82013-12-02 23:08:03 +00003897def CMDweb(parser, args):
3898 """Opens the current CL in the web browser."""
3899 _, args = parser.parse_args(args)
3900 if args:
3901 parser.error('Unrecognized args: %s' % ' '.join(args))
3902
3903 issue_url = Changelist().GetIssueURL()
3904 if not issue_url:
3905 print >> sys.stderr, 'ERROR No issue to open'
3906 return 1
3907
3908 webbrowser.open(issue_url)
3909 return 0
3910
3911
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003912def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003913 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003914 auth.add_auth_options(parser)
3915 options, args = parser.parse_args(args)
3916 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003917 if args:
3918 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003919 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003920 props = cl.GetIssueProperties()
3921 if props.get('private'):
3922 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003923 cl.SetFlag('commit', '1')
3924 return 0
3925
3926
groby@chromium.org411034a2013-02-26 15:12:01 +00003927def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003928 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003929 auth.add_auth_options(parser)
3930 options, args = parser.parse_args(args)
3931 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003932 if args:
3933 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003934 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003935 # Ensure there actually is an issue to close.
3936 cl.GetDescription()
3937 cl.CloseIssue()
3938 return 0
3939
3940
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003941def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003942 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003943 auth.add_auth_options(parser)
3944 options, args = parser.parse_args(args)
3945 auth_config = auth.extract_auth_config_from_options(options)
3946 if args:
3947 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003948
3949 # Uncommitted (staged and unstaged) changes will be destroyed by
3950 # "git reset --hard" if there are merging conflicts in PatchIssue().
3951 # Staged changes would be committed along with the patch from last
3952 # upload, hence counted toward the "last upload" side in the final
3953 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003954 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003955 return 1
3956
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003957 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003958 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003959 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003960 if not issue:
3961 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003962 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003963 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003964
3965 # Create a new branch based on the merge-base
3966 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3967 try:
3968 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003969 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003970 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003971 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003972 return rtn
3973
wychen@chromium.org06928532015-02-03 02:11:29 +00003974 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003975 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003976 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003977 finally:
3978 RunGit(['checkout', '-q', branch])
3979 RunGit(['branch', '-D', TMP_BRANCH])
3980
3981 return 0
3982
3983
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003984def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003985 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003986 parser.add_option(
3987 '--no-color',
3988 action='store_true',
3989 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003990 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003991 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003992 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003993
3994 author = RunGit(['config', 'user.email']).strip() or None
3995
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003996 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003997
3998 if args:
3999 if len(args) > 1:
4000 parser.error('Unknown args')
4001 base_branch = args[0]
4002 else:
4003 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004004 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004005
4006 change = cl.GetChange(base_branch, None)
4007 return owners_finder.OwnersFinder(
4008 [f.LocalPath() for f in
4009 cl.GetChange(base_branch, None).AffectedFiles()],
4010 change.RepositoryRoot(), author,
4011 fopen=file, os_path=os.path, glob=glob.glob,
4012 disable_color=options.no_color).run()
4013
4014
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004015def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004016 """Generates a diff command."""
4017 # Generate diff for the current branch's changes.
4018 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4019 upstream_commit, '--' ]
4020
4021 if args:
4022 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004023 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004024 diff_cmd.append(arg)
4025 else:
4026 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004027
4028 return diff_cmd
4029
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004030def MatchingFileType(file_name, extensions):
4031 """Returns true if the file name ends with one of the given extensions."""
4032 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004033
enne@chromium.org555cfe42014-01-29 18:21:39 +00004034@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004035def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004036 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004037 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004038 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004039 parser.add_option('--full', action='store_true',
4040 help='Reformat the full content of all touched files')
4041 parser.add_option('--dry-run', action='store_true',
4042 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004043 parser.add_option('--python', action='store_true',
4044 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004045 parser.add_option('--diff', action='store_true',
4046 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004047 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004048
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004049 # git diff generates paths against the root of the repository. Change
4050 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004051 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004052 if rel_base_path:
4053 os.chdir(rel_base_path)
4054
digit@chromium.org29e47272013-05-17 17:01:46 +00004055 # Grab the merge-base commit, i.e. the upstream commit of the current
4056 # branch when it was created or the last time it was rebased. This is
4057 # to cover the case where the user may have called "git fetch origin",
4058 # moving the origin branch to a newer commit, but hasn't rebased yet.
4059 upstream_commit = None
4060 cl = Changelist()
4061 upstream_branch = cl.GetUpstreamBranch()
4062 if upstream_branch:
4063 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4064 upstream_commit = upstream_commit.strip()
4065
4066 if not upstream_commit:
4067 DieWithError('Could not find base commit for this branch. '
4068 'Are you in detached state?')
4069
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004070 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4071 diff_output = RunGit(changed_files_cmd)
4072 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004073 # Filter out files deleted by this CL
4074 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004075
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004076 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4077 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4078 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004079 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004080
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004081 top_dir = os.path.normpath(
4082 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4083
4084 # Locate the clang-format binary in the checkout
4085 try:
4086 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4087 except clang_format.NotFoundError, e:
4088 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004089
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004090 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4091 # formatted. This is used to block during the presubmit.
4092 return_value = 0
4093
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004094 if clang_diff_files:
4095 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004096 cmd = [clang_format_tool]
4097 if not opts.dry_run and not opts.diff:
4098 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004099 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004100 if opts.diff:
4101 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004102 else:
4103 env = os.environ.copy()
4104 env['PATH'] = str(os.path.dirname(clang_format_tool))
4105 try:
4106 script = clang_format.FindClangFormatScriptInChromiumTree(
4107 'clang-format-diff.py')
4108 except clang_format.NotFoundError, e:
4109 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004110
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004111 cmd = [sys.executable, script, '-p0']
4112 if not opts.dry_run and not opts.diff:
4113 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004114
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004115 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4116 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004117
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004118 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4119 if opts.diff:
4120 sys.stdout.write(stdout)
4121 if opts.dry_run and len(stdout) > 0:
4122 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004123
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004124 # Similar code to above, but using yapf on .py files rather than clang-format
4125 # on C/C++ files
4126 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004127 yapf_tool = gclient_utils.FindExecutable('yapf')
4128 if yapf_tool is None:
4129 DieWithError('yapf not found in PATH')
4130
4131 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004132 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004133 cmd = [yapf_tool]
4134 if not opts.dry_run and not opts.diff:
4135 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004136 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004137 if opts.diff:
4138 sys.stdout.write(stdout)
4139 else:
4140 # TODO(sbc): yapf --lines mode still has some issues.
4141 # https://github.com/google/yapf/issues/154
4142 DieWithError('--python currently only works with --full')
4143
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004144 # Dart's formatter does not have the nice property of only operating on
4145 # modified chunks, so hard code full.
4146 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004147 try:
4148 command = [dart_format.FindDartFmtToolInChromiumTree()]
4149 if not opts.dry_run and not opts.diff:
4150 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004151 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004152
ppi@chromium.org6593d932016-03-03 15:41:15 +00004153 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004154 if opts.dry_run and stdout:
4155 return_value = 2
4156 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004157 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4158 'found in this checkout. Files in other languages are still ' +
4159 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004160
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004161 # Format GN build files. Always run on full build files for canonical form.
4162 if gn_diff_files:
4163 cmd = ['gn', 'format']
4164 if not opts.dry_run and not opts.diff:
4165 cmd.append('--in-place')
4166 for gn_diff_file in gn_diff_files:
4167 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4168 if opts.diff:
4169 sys.stdout.write(stdout)
4170
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004171 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004172
4173
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004174@subcommand.usage('<codereview url or issue id>')
4175def CMDcheckout(parser, args):
4176 """Checks out a branch associated with a given Rietveld issue."""
4177 _, args = parser.parse_args(args)
4178
4179 if len(args) != 1:
4180 parser.print_help()
4181 return 1
4182
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004183 target_issue = ParseIssueNum(args[0])
4184 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004185 parser.print_help()
4186 return 1
4187
4188 key_and_issues = [x.split() for x in RunGit(
4189 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4190 .splitlines()]
4191 branches = []
4192 for key, issue in key_and_issues:
4193 if issue == target_issue:
4194 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4195
4196 if len(branches) == 0:
4197 print 'No branch found for issue %s.' % target_issue
4198 return 1
4199 if len(branches) == 1:
4200 RunGit(['checkout', branches[0]])
4201 else:
4202 print 'Multiple branches match issue %s:' % target_issue
4203 for i in range(len(branches)):
4204 print '%d: %s' % (i, branches[i])
4205 which = raw_input('Choose by index: ')
4206 try:
4207 RunGit(['checkout', branches[int(which)]])
4208 except (IndexError, ValueError):
4209 print 'Invalid selection, not checking out any branch.'
4210 return 1
4211
4212 return 0
4213
4214
maruel@chromium.org29404b52014-09-08 22:58:00 +00004215def CMDlol(parser, args):
4216 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004217 print zlib.decompress(base64.b64decode(
4218 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4219 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4220 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4221 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004222 return 0
4223
4224
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004225class OptionParser(optparse.OptionParser):
4226 """Creates the option parse and add --verbose support."""
4227 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004228 optparse.OptionParser.__init__(
4229 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004230 self.add_option(
4231 '-v', '--verbose', action='count', default=0,
4232 help='Use 2 times for more debugging info')
4233
4234 def parse_args(self, args=None, values=None):
4235 options, args = optparse.OptionParser.parse_args(self, args, values)
4236 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4237 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4238 return options, args
4239
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004241def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004242 if sys.hexversion < 0x02060000:
4243 print >> sys.stderr, (
4244 '\nYour python version %s is unsupported, please upgrade.\n' %
4245 sys.version.split(' ', 1)[0])
4246 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004247
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004248 # Reload settings.
4249 global settings
4250 settings = Settings()
4251
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004252 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004253 dispatcher = subcommand.CommandDispatcher(__name__)
4254 try:
4255 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004256 except auth.AuthenticationError as e:
4257 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004258 except urllib2.HTTPError, e:
4259 if e.code != 500:
4260 raise
4261 DieWithError(
4262 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4263 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004264 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004265
4266
4267if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004268 # These affect sys.stdout so do it outside of main() to simplify mocks in
4269 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004270 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004271 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004272 try:
4273 sys.exit(main(sys.argv[1:]))
4274 except KeyboardInterrupt:
4275 sys.stderr.write('interrupted\n')
4276 sys.exit(1)