blob: 98a2f01b44485b746fb82c3ad290d5f379e3eab7 [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.orgc4a94542016-03-24 17:33:00 +0000897 # Automatic selection.
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000898 assert not self.issue
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +0000899 # Check if this branch is associated with Rietveld => Rieveld.
900 self._codereview_impl = _RietveldChangelistImpl(self, **kwargs)
901 if self.GetIssue(force_lookup=True):
902 return
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000903
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +0000904 tmp_rietveld = self._codereview_impl # Save Rietveld object.
905
906 # Check if this branch has Gerrit issue associated => Gerrit.
907 self._codereview_impl = _GerritChangelistImpl(self, **kwargs)
908 if self.GetIssue(force_lookup=True):
909 return
910
911 # OK, no issue is set for this branch.
912 # If Gerrit is set repo-wide => Gerrit.
913 if settings.GetIsGerrit():
914 return
915
916 self._codereview_impl = tmp_rietveld
917 return
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000918
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000919
920 def GetCCList(self):
921 """Return the users cc'd on this CL.
922
923 Return is a string suitable for passing to gcl with the --cc flag.
924 """
925 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000926 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000927 more_cc = ','.join(self.watchers)
928 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
929 return self.cc
930
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000931 def GetCCListWithoutDefault(self):
932 """Return the users cc'd on this CL excluding default ones."""
933 if self.cc is None:
934 self.cc = ','.join(self.watchers)
935 return self.cc
936
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000937 def SetWatchers(self, watchers):
938 """Set the list of email addresses that should be cc'd based on the changed
939 files in this CL.
940 """
941 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942
943 def GetBranch(self):
944 """Returns the short branch name, e.g. 'master'."""
945 if not self.branch:
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000946 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000947 if not branchref:
948 return None
949 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 self.branch = ShortBranchName(self.branchref)
951 return self.branch
952
953 def GetBranchRef(self):
954 """Returns the full branch name, e.g. 'refs/heads/master'."""
955 self.GetBranch() # Poke the lazy loader.
956 return self.branchref
957
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000958 @staticmethod
959 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000960 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961 e.g. 'origin', 'refs/heads/master'
962 """
963 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000964 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
965 error_ok=True).strip()
966 if upstream_branch:
967 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
968 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000969 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
970 error_ok=True).strip()
971 if upstream_branch:
972 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000974 # Fall back on trying a git-svn upstream branch.
975 if settings.GetIsGitSvn():
976 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000978 # Else, try to guess the origin remote.
979 remote_branches = RunGit(['branch', '-r']).split()
980 if 'origin/master' in remote_branches:
981 # Fall back on origin/master if it exits.
982 remote = 'origin'
983 upstream_branch = 'refs/heads/master'
984 elif 'origin/trunk' in remote_branches:
985 # Fall back on origin/trunk if it exists. Generally a shared
986 # git-svn clone
987 remote = 'origin'
988 upstream_branch = 'refs/heads/trunk'
989 else:
tandrii@chromium.orge044c812016-03-24 10:13:29 +0000990 DieWithError(
991 'Unable to determine default branch to diff against.\n'
992 'Either pass complete "git diff"-style arguments, like\n'
993 ' git cl upload origin/master\n'
994 'or verify this branch is set up to track another \n'
995 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996
997 return remote, upstream_branch
998
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000999 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001000 upstream_branch = self.GetUpstreamBranch()
1001 if not BranchExists(upstream_branch):
1002 DieWithError('The upstream for the current branch (%s) does not exist '
1003 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001004 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001005 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007 def GetUpstreamBranch(self):
1008 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001009 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001011 upstream_branch = upstream_branch.replace('refs/heads/',
1012 'refs/remotes/%s/' % remote)
1013 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1014 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001015 self.upstream_branch = upstream_branch
1016 return self.upstream_branch
1017
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001018 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001019 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001020 remote, branch = None, self.GetBranch()
1021 seen_branches = set()
1022 while branch not in seen_branches:
1023 seen_branches.add(branch)
1024 remote, branch = self.FetchUpstreamTuple(branch)
1025 branch = ShortBranchName(branch)
1026 if remote != '.' or branch.startswith('refs/remotes'):
1027 break
1028 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001029 remotes = RunGit(['remote'], error_ok=True).split()
1030 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001031 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001032 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001033 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001034 logging.warning('Could not determine which remote this change is '
1035 'associated with, so defaulting to "%s". This may '
1036 'not be what you want. You may prevent this message '
1037 'by running "git svn info" as documented here: %s',
1038 self._remote,
1039 GIT_INSTRUCTIONS_URL)
1040 else:
1041 logging.warn('Could not determine which remote this change is '
1042 'associated with. You may prevent this message by '
1043 'running "git svn info" as documented here: %s',
1044 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001045 branch = 'HEAD'
1046 if branch.startswith('refs/remotes'):
1047 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001048 elif branch.startswith('refs/branch-heads/'):
1049 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 else:
1051 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001052 return self._remote
1053
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001054 def GitSanityChecks(self, upstream_git_obj):
1055 """Checks git repo status and ensures diff is from local commits."""
1056
sbc@chromium.org79706062015-01-14 21:18:12 +00001057 if upstream_git_obj is None:
1058 if self.GetBranch() is None:
1059 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001060 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001061 else:
1062 print >> sys.stderr, (
1063 'ERROR: no upstream branch')
1064 return False
1065
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001066 # Verify the commit we're diffing against is in our current branch.
1067 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1068 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1069 if upstream_sha != common_ancestor:
1070 print >> sys.stderr, (
1071 'ERROR: %s is not in the current branch. You may need to rebase '
1072 'your tracking branch' % upstream_sha)
1073 return False
1074
1075 # List the commits inside the diff, and verify they are all local.
1076 commits_in_diff = RunGit(
1077 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1078 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1079 remote_branch = remote_branch.strip()
1080 if code != 0:
1081 _, remote_branch = self.GetRemoteBranch()
1082
1083 commits_in_remote = RunGit(
1084 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1085
1086 common_commits = set(commits_in_diff) & set(commits_in_remote)
1087 if common_commits:
1088 print >> sys.stderr, (
1089 'ERROR: Your diff contains %d commits already in %s.\n'
1090 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1091 'the diff. If you are using a custom git flow, you can override'
1092 ' the reference used for this check with "git config '
1093 'gitcl.remotebranch <git-ref>".' % (
1094 len(common_commits), remote_branch, upstream_git_obj))
1095 return False
1096 return True
1097
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001098 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001099 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001100
1101 Returns None if it is not set.
1102 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001103 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1104 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001105
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001106 def GetGitSvnRemoteUrl(self):
1107 """Return the configured git-svn remote URL parsed from git svn info.
1108
1109 Returns None if it is not set.
1110 """
1111 # URL is dependent on the current directory.
1112 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1113 if data:
1114 keys = dict(line.split(': ', 1) for line in data.splitlines()
1115 if ': ' in line)
1116 return keys.get('URL', None)
1117 return None
1118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 def GetRemoteUrl(self):
1120 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1121
1122 Returns None if there is no remote.
1123 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001124 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001125 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1126
1127 # If URL is pointing to a local directory, it is probably a git cache.
1128 if os.path.isdir(url):
1129 url = RunGit(['config', 'remote.%s.url' % remote],
1130 error_ok=True,
1131 cwd=url).strip()
1132 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +00001134 def GetIssue(self, force_lookup=False):
maruel@chromium.org52424302012-08-29 15:14:30 +00001135 """Returns the issue number as a int or None if not set."""
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +00001136 if force_lookup or (self.issue is None and not self.lookedup_issue):
1137 issue = RunGit(['config', self._codereview_impl.IssueSetting()],
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001138 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.issue = int(issue) or None if issue else None
1140 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 return self.issue
1142
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 def GetIssueURL(self):
1144 """Get the URL for a particular issue."""
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001145 issue = self.GetIssue()
1146 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001147 return None
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001148 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149
1150 def GetDescription(self, pretty=False):
1151 if not self.has_description:
1152 if self.GetIssue():
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001153 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 self.has_description = True
1155 if pretty:
1156 wrapper = textwrap.TextWrapper()
1157 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1158 return wrapper.fill(self.description)
1159 return self.description
1160
1161 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001162 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001163 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001164 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001166 self.patchset = int(patchset) or None if patchset else None
1167 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168 return self.patchset
1169
1170 def SetPatchset(self, patchset):
1171 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001172 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 if patchset:
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001174 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001175 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 else:
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001177 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001178 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001179 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001181 def SetIssue(self, issue=None):
1182 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +00001183 issue_setting = self._codereview_impl.IssueSetting()
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001184 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001186 self.issue = issue
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001187 RunGit(['config', issue_setting, str(issue)])
1188 codereview_server = self._codereview_impl.GetCodereviewServer()
1189 if codereview_server:
1190 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001192 current_issue = self.GetIssue()
1193 if current_issue:
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001194 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001195 self.issue = None
1196 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001198 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001199 if not self.GitSanityChecks(upstream_branch):
1200 DieWithError('\nGit sanity check failure')
1201
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001202 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001203 if not root:
1204 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001205 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001206
1207 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001208 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001209 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001210 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001211 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001212 except subprocess2.CalledProcessError:
1213 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001214 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001215 'This branch probably doesn\'t exist anymore. To reset the\n'
1216 'tracking branch, please run\n'
1217 ' git branch --set-upstream %s trunk\n'
1218 'replacing trunk with origin/master or the relevant branch') %
1219 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001220
maruel@chromium.org52424302012-08-29 15:14:30 +00001221 issue = self.GetIssue()
1222 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001223 if issue:
1224 description = self.GetDescription()
1225 else:
1226 # If the change was never uploaded, use the log messages of all commits
1227 # up to the branch point, as git cl upload will prefill the description
1228 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001229 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1230 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001231
1232 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001233 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001234 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001235 name,
1236 description,
1237 absroot,
1238 files,
1239 issue,
1240 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001241 author,
1242 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001243
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001244 def UpdateDescription(self, description):
1245 self.description = description
1246 return self._codereview_impl.UpdateDescriptionRemote(description)
1247
1248 def RunHook(self, committing, may_prompt, verbose, change):
1249 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1250 try:
1251 return presubmit_support.DoPresubmitChecks(change, committing,
1252 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1253 default_presubmit=None, may_prompt=may_prompt,
1254 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1255 except presubmit_support.PresubmitFailure, e:
1256 DieWithError(
1257 ('%s\nMaybe your depot_tools is out of date?\n'
1258 'If all fails, contact maruel@') % e)
1259
1260 # Forward methods to codereview specific implementation.
1261
1262 def CloseIssue(self):
1263 return self._codereview_impl.CloseIssue()
1264
1265 def GetStatus(self):
1266 return self._codereview_impl.GetStatus()
1267
1268 def GetCodereviewServer(self):
1269 return self._codereview_impl.GetCodereviewServer()
1270
1271 def GetApprovingReviewers(self):
1272 return self._codereview_impl.GetApprovingReviewers()
1273
1274 def GetMostRecentPatchset(self):
1275 return self._codereview_impl.GetMostRecentPatchset()
1276
1277 def __getattr__(self, attr):
1278 # This is because lots of untested code accesses Rietveld-specific stuff
1279 # directly, and it's hard to fix for sure. So, just let it work, and fix
1280 # on a cases by case basis.
1281 return getattr(self._codereview_impl, attr)
1282
1283
1284class _ChangelistCodereviewBase(object):
1285 """Abstract base class encapsulating codereview specifics of a changelist."""
1286 def __init__(self, changelist):
1287 self._changelist = changelist # instance of Changelist
1288
1289 def __getattr__(self, attr):
1290 # Forward methods to changelist.
1291 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1292 # _RietveldChangelistImpl to avoid this hack?
1293 return getattr(self._changelist, attr)
1294
1295 def GetStatus(self):
1296 """Apply a rough heuristic to give a simple summary of an issue's review
1297 or CQ status, assuming adherence to a common workflow.
1298
1299 Returns None if no issue for this branch, or specific string keywords.
1300 """
1301 raise NotImplementedError()
1302
1303 def GetCodereviewServer(self):
1304 """Returns server URL without end slash, like "https://codereview.com"."""
1305 raise NotImplementedError()
1306
1307 def FetchDescription(self):
1308 """Fetches and returns description from the codereview server."""
1309 raise NotImplementedError()
1310
1311 def GetCodereviewServerSetting(self):
1312 """Returns git config setting for the codereview server."""
1313 raise NotImplementedError()
1314
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +00001315 def IssueSetting(self):
1316 """Returns name of git config setting which stores issue number."""
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001317 raise NotImplementedError()
1318
1319 def PatchsetSetting(self):
1320 """Returns name of git config setting which stores issue number."""
1321 raise NotImplementedError()
1322
1323 def GetRieveldObjForPresubmit(self):
1324 # This is an unfortunate Rietveld-embeddedness in presubmit.
1325 # For non-Rietveld codereviews, this probably should return a dummy object.
1326 raise NotImplementedError()
1327
1328 def UpdateDescriptionRemote(self, description):
1329 """Update the description on codereview site."""
1330 raise NotImplementedError()
1331
1332 def CloseIssue(self):
1333 """Closes the issue."""
1334 raise NotImplementedError()
1335
1336 def GetApprovingReviewers(self):
1337 """Returns a list of reviewers approving the change.
1338
1339 Note: not necessarily committers.
1340 """
1341 raise NotImplementedError()
1342
1343 def GetMostRecentPatchset(self):
1344 """Returns the most recent patchset number from the codereview site."""
1345 raise NotImplementedError()
1346
1347
1348class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1349 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1350 super(_RietveldChangelistImpl, self).__init__(changelist)
1351 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1352 settings.GetDefaultServerUrl()
1353
1354 self._rietveld_server = rietveld_server
1355 self._auth_config = auth_config
1356 self._props = None
1357 self._rpc_server = None
1358
1359 def GetAuthConfig(self):
1360 return self._auth_config
1361
1362 def GetCodereviewServer(self):
1363 if not self._rietveld_server:
1364 # If we're on a branch then get the server potentially associated
1365 # with that branch.
1366 if self.GetIssue():
1367 rietveld_server_setting = self.GetCodereviewServerSetting()
1368 if rietveld_server_setting:
1369 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1370 ['config', rietveld_server_setting], error_ok=True).strip())
1371 if not self._rietveld_server:
1372 self._rietveld_server = settings.GetDefaultServerUrl()
1373 return self._rietveld_server
1374
1375 def FetchDescription(self):
1376 issue = self.GetIssue()
1377 assert issue
1378 try:
1379 return self.RpcServer().get_description(issue).strip()
1380 except urllib2.HTTPError as e:
1381 if e.code == 404:
1382 DieWithError(
1383 ('\nWhile fetching the description for issue %d, received a '
1384 '404 (not found)\n'
1385 'error. It is likely that you deleted this '
1386 'issue on the server. If this is the\n'
1387 'case, please run\n\n'
1388 ' git cl issue 0\n\n'
1389 'to clear the association with the deleted issue. Then run '
1390 'this command again.') % issue)
1391 else:
1392 DieWithError(
1393 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1394 except urllib2.URLError as e:
1395 print >> sys.stderr, (
1396 'Warning: Failed to retrieve CL description due to network '
1397 'failure.')
1398 return ''
1399
1400 def GetMostRecentPatchset(self):
1401 return self.GetIssueProperties()['patchsets'][-1]
1402
1403 def GetPatchSetDiff(self, issue, patchset):
1404 return self.RpcServer().get(
1405 '/download/issue%s_%s.diff' % (issue, patchset))
1406
1407 def GetIssueProperties(self):
1408 if self._props is None:
1409 issue = self.GetIssue()
1410 if not issue:
1411 self._props = {}
1412 else:
1413 self._props = self.RpcServer().get_issue_properties(issue, True)
1414 return self._props
1415
1416 def GetApprovingReviewers(self):
1417 return get_approving_reviewers(self.GetIssueProperties())
1418
1419 def AddComment(self, message):
1420 return self.RpcServer().add_comment(self.GetIssue(), message)
1421
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001422 def GetStatus(self):
1423 """Apply a rough heuristic to give a simple summary of an issue's review
1424 or CQ status, assuming adherence to a common workflow.
1425
1426 Returns None if no issue for this branch, or one of the following keywords:
1427 * 'error' - error from review tool (including deleted issues)
1428 * 'unsent' - not sent for review
1429 * 'waiting' - waiting for review
1430 * 'reply' - waiting for owner to reply to review
1431 * 'lgtm' - LGTM from at least one approved reviewer
1432 * 'commit' - in the commit queue
1433 * 'closed' - closed
1434 """
1435 if not self.GetIssue():
1436 return None
1437
1438 try:
1439 props = self.GetIssueProperties()
1440 except urllib2.HTTPError:
1441 return 'error'
1442
1443 if props.get('closed'):
1444 # Issue is closed.
1445 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001446 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001447 # Issue is in the commit queue.
1448 return 'commit'
1449
1450 try:
1451 reviewers = self.GetApprovingReviewers()
1452 except urllib2.HTTPError:
1453 return 'error'
1454
1455 if reviewers:
1456 # Was LGTM'ed.
1457 return 'lgtm'
1458
1459 messages = props.get('messages') or []
1460
1461 if not messages:
1462 # No message was sent.
1463 return 'unsent'
1464 if messages[-1]['sender'] != props.get('owner_email'):
1465 # Non-LGTM reply from non-owner
1466 return 'reply'
1467 return 'waiting'
1468
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001469 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001470 return self.RpcServer().update_description(
1471 self.GetIssue(), self.description)
1472
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001474 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001476 def SetFlag(self, flag, value):
1477 """Patchset must match."""
1478 if not self.GetPatchset():
1479 DieWithError('The patchset needs to match. Send another patchset.')
1480 try:
1481 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001482 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001483 except urllib2.HTTPError, e:
1484 if e.code == 404:
1485 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1486 if e.code == 403:
1487 DieWithError(
1488 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1489 'match?') % (self.GetIssue(), self.GetPatchset()))
1490 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001492 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493 """Returns an upload.RpcServer() to access this review's rietveld instance.
1494 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001495 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001496 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001497 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001498 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001499 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +00001501 def IssueSetting(self):
1502 """Return the git setting that stores this change's issue."""
1503 return 'branch.%s.rietveldissue' % self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001505 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 """Return the git setting that stores this change's most recent patchset."""
1507 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1508
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001509 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001511 branch = self.GetBranch()
1512 if branch:
1513 return 'branch.%s.rietveldserver' % branch
1514 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001516 def GetRieveldObjForPresubmit(self):
1517 return self.RpcServer()
1518
1519
1520class _GerritChangelistImpl(_ChangelistCodereviewBase):
1521 def __init__(self, changelist, auth_config=None):
1522 # auth_config is Rietveld thing, kept here to preserve interface only.
1523 super(_GerritChangelistImpl, self).__init__(changelist)
1524 self._change_id = None
1525 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1526 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1527
1528 def _GetGerritHost(self):
1529 # Lazy load of configs.
1530 self.GetCodereviewServer()
1531 return self._gerrit_host
1532
1533 def GetCodereviewServer(self):
1534 if not self._gerrit_server:
1535 # If we're on a branch then get the server potentially associated
1536 # with that branch.
1537 if self.GetIssue():
1538 gerrit_server_setting = self.GetCodereviewServerSetting()
1539 if gerrit_server_setting:
1540 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1541 error_ok=True).strip()
1542 if self._gerrit_server:
1543 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1544 if not self._gerrit_server:
1545 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1546 # has "-review" suffix for lowest level subdomain.
1547 parts = urlparse.urlparse(self.GetRemoteUrl()).netloc.split('.')
1548 parts[0] = parts[0] + '-review'
1549 self._gerrit_host = '.'.join(parts)
1550 self._gerrit_server = 'https://%s' % self._gerrit_host
1551 return self._gerrit_server
1552
tandrii@chromium.orgc4a94542016-03-24 17:33:00 +00001553 def IssueSetting(self):
1554 """Return the git setting that stores this change's issue."""
1555 return 'branch.%s.gerritissue' % self.GetBranch()
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001556
1557 def PatchsetSetting(self):
1558 """Return the git setting that stores this change's most recent patchset."""
1559 return 'branch.%s.gerritpatchset' % self.GetBranch()
1560
1561 def GetCodereviewServerSetting(self):
1562 """Returns the git setting that stores this change's Gerrit server."""
1563 branch = self.GetBranch()
1564 if branch:
1565 return 'branch.%s.gerritserver' % branch
1566 return None
1567
1568 def GetRieveldObjForPresubmit(self):
1569 class ThisIsNotRietveldIssue(object):
1570 def __getattr__(self, attr):
1571 print(
1572 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
1573 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
1574 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
1575 'or use Rietveld for codereview.\n' % attr)
1576 raise NotImplementedError()
1577 return ThisIsNotRietveldIssue()
1578
1579 def GetStatus(self):
1580 # TODO(tandrii)
1581 raise NotImplementedError()
1582
1583 def GetMostRecentPatchset(self):
1584 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1585 ['CURRENT_REVISION'])
1586 return data['revisions'][data['current_revision']]['_number']
1587
1588 def FetchDescription(self):
1589 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), self.GetIssue(),
1590 ['COMMIT_FOOTERS', 'CURRENT_REVISION'])
1591 return data['revisions'][data['current_revision']]['commit_with_footers']
1592
1593 def UpdateDescriptionRemote(self, description):
1594 # TODO(tandrii)
1595 raise NotImplementedError()
1596
1597 def CloseIssue(self):
tandrii@chromium.orgeba7b7e2016-03-24 17:32:34 +00001598 # TODO(tandrii)
1599 raise NotImplementedError()
tandrii@chromium.orge044c812016-03-24 10:13:29 +00001600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001601
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001602class ChangeDescription(object):
1603 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001604 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001605 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001606
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001607 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001608 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001609
agable@chromium.org42c20792013-09-12 17:34:49 +00001610 @property # www.logilab.org/ticket/89786
1611 def description(self): # pylint: disable=E0202
1612 return '\n'.join(self._description_lines)
1613
1614 def set_description(self, desc):
1615 if isinstance(desc, basestring):
1616 lines = desc.splitlines()
1617 else:
1618 lines = [line.rstrip() for line in desc]
1619 while lines and not lines[0]:
1620 lines.pop(0)
1621 while lines and not lines[-1]:
1622 lines.pop(-1)
1623 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001624
piman@chromium.org336f9122014-09-04 02:16:55 +00001625 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001626 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001627 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001628 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001629 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001630 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001631
agable@chromium.org42c20792013-09-12 17:34:49 +00001632 # Get the set of R= and TBR= lines and remove them from the desciption.
1633 regexp = re.compile(self.R_LINE)
1634 matches = [regexp.match(line) for line in self._description_lines]
1635 new_desc = [l for i, l in enumerate(self._description_lines)
1636 if not matches[i]]
1637 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001638
agable@chromium.org42c20792013-09-12 17:34:49 +00001639 # Construct new unified R= and TBR= lines.
1640 r_names = []
1641 tbr_names = []
1642 for match in matches:
1643 if not match:
1644 continue
1645 people = cleanup_list([match.group(2).strip()])
1646 if match.group(1) == 'TBR':
1647 tbr_names.extend(people)
1648 else:
1649 r_names.extend(people)
1650 for name in r_names:
1651 if name not in reviewers:
1652 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001653 if add_owners_tbr:
1654 owners_db = owners.Database(change.RepositoryRoot(),
1655 fopen=file, os_path=os.path, glob=glob.glob)
1656 all_reviewers = set(tbr_names + reviewers)
1657 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1658 all_reviewers)
1659 tbr_names.extend(owners_db.reviewers_for(missing_files,
1660 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001661 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1662 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1663
1664 # Put the new lines in the description where the old first R= line was.
1665 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1666 if 0 <= line_loc < len(self._description_lines):
1667 if new_tbr_line:
1668 self._description_lines.insert(line_loc, new_tbr_line)
1669 if new_r_line:
1670 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001671 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001672 if new_r_line:
1673 self.append_footer(new_r_line)
1674 if new_tbr_line:
1675 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001676
1677 def prompt(self):
1678 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001679 self.set_description([
1680 '# Enter a description of the change.',
1681 '# This will be displayed on the codereview site.',
1682 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001683 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001684 '--------------------',
1685 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001686
agable@chromium.org42c20792013-09-12 17:34:49 +00001687 regexp = re.compile(self.BUG_LINE)
1688 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001689 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001690 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001691 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001692 if not content:
1693 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001694 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001695
1696 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001697 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1698 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001699 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001700 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001701
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001702 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001703 if self._description_lines:
1704 # Add an empty line if either the last line or the new line isn't a tag.
1705 last_line = self._description_lines[-1]
1706 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1707 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1708 self._description_lines.append('')
1709 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001710
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001711 def get_reviewers(self):
1712 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001713 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1714 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001715 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001716
1717
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001718def get_approving_reviewers(props):
1719 """Retrieves the reviewers that approved a CL from the issue properties with
1720 messages.
1721
1722 Note that the list may contain reviewers that are not committer, thus are not
1723 considered by the CQ.
1724 """
1725 return sorted(
1726 set(
1727 message['sender']
1728 for message in props['messages']
1729 if message['approval'] and message['sender'] in props['reviewers']
1730 )
1731 )
1732
1733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001734def FindCodereviewSettingsFile(filename='codereview.settings'):
1735 """Finds the given file starting in the cwd and going up.
1736
1737 Only looks up to the top of the repository unless an
1738 'inherit-review-settings-ok' file exists in the root of the repository.
1739 """
1740 inherit_ok_file = 'inherit-review-settings-ok'
1741 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001742 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1744 root = '/'
1745 while True:
1746 if filename in os.listdir(cwd):
1747 if os.path.isfile(os.path.join(cwd, filename)):
1748 return open(os.path.join(cwd, filename))
1749 if cwd == root:
1750 break
1751 cwd = os.path.dirname(cwd)
1752
1753
1754def LoadCodereviewSettingsFromFile(fileobj):
1755 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001756 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001757
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001758 def SetProperty(name, setting, unset_error_ok=False):
1759 fullname = 'rietveld.' + name
1760 if setting in keyvals:
1761 RunGit(['config', fullname, keyvals[setting]])
1762 else:
1763 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1764
1765 SetProperty('server', 'CODE_REVIEW_SERVER')
1766 # Only server setting is required. Other settings can be absent.
1767 # In that case, we ignore errors raised during option deletion attempt.
1768 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001769 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001770 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1771 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001772 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001773 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001774 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1775 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001776 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001777 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001778 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001779 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1780 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001782 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001783 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001784
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001785 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1786 RunGit(['config', 'gerrit.squash-uploads',
1787 keyvals['GERRIT_SQUASH_UPLOADS']])
1788
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1790 #should be of the form
1791 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1792 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1793 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1794 keyvals['ORIGIN_URL_CONFIG']])
1795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001797def urlretrieve(source, destination):
1798 """urllib is broken for SSL connections via a proxy therefore we
1799 can't use urllib.urlretrieve()."""
1800 with open(destination, 'w') as f:
1801 f.write(urllib2.urlopen(source).read())
1802
1803
ukai@chromium.org712d6102013-11-27 00:52:58 +00001804def hasSheBang(fname):
1805 """Checks fname is a #! script."""
1806 with open(fname) as f:
1807 return f.read(2).startswith('#!')
1808
1809
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001810def DownloadGerritHook(force):
1811 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001812
1813 Args:
1814 force: True to update hooks. False to install hooks if not present.
1815 """
1816 if not settings.GetIsGerrit():
1817 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001818 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001819 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1820 if not os.access(dst, os.X_OK):
1821 if os.path.exists(dst):
1822 if not force:
1823 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001824 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00001825 print(
1826 'WARNING: installing Gerrit commit-msg hook.\n'
1827 ' This behavior of git cl will soon be disabled.\n'
1828 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001829 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001830 if not hasSheBang(dst):
1831 DieWithError('Not a script: %s\n'
1832 'You need to download from\n%s\n'
1833 'into .git/hooks/commit-msg and '
1834 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001835 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1836 except Exception:
1837 if os.path.exists(dst):
1838 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001839 DieWithError('\nFailed to download hooks.\n'
1840 'You need to download from\n%s\n'
1841 'into .git/hooks/commit-msg and '
1842 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001843
1844
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001845
1846def GetRietveldCodereviewSettingsInteractively():
1847 """Prompt the user for settings."""
1848 server = settings.GetDefaultServerUrl(error_ok=True)
1849 prompt = 'Rietveld server (host[:port])'
1850 prompt += ' [%s]' % (server or DEFAULT_SERVER)
1851 newserver = ask_for_data(prompt + ':')
1852 if not server and not newserver:
1853 newserver = DEFAULT_SERVER
1854 if newserver:
1855 newserver = gclient_utils.UpgradeToHttps(newserver)
1856 if newserver != server:
1857 RunGit(['config', 'rietveld.server', newserver])
1858
1859 def SetProperty(initial, caption, name, is_url):
1860 prompt = caption
1861 if initial:
1862 prompt += ' ("x" to clear) [%s]' % initial
1863 new_val = ask_for_data(prompt + ':')
1864 if new_val == 'x':
1865 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
1866 elif new_val:
1867 if is_url:
1868 new_val = gclient_utils.UpgradeToHttps(new_val)
1869 if new_val != initial:
1870 RunGit(['config', 'rietveld.' + name, new_val])
1871
1872 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
1873 SetProperty(settings.GetDefaultPrivateFlag(),
1874 'Private flag (rietveld only)', 'private', False)
1875 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
1876 'tree-status-url', False)
1877 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
1878 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
1879 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1880 'run-post-upload-hook', False)
1881
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001882@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001883def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001884 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001885
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001886 print('WARNING: git cl config works for Rietveld only.\n'
1887 'For Gerrit, see http://crbug.com/579160.')
1888 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001889 parser.add_option('--activate-update', action='store_true',
1890 help='activate auto-updating [rietveld] section in '
1891 '.git/config')
1892 parser.add_option('--deactivate-update', action='store_true',
1893 help='deactivate auto-updating [rietveld] section in '
1894 '.git/config')
1895 options, args = parser.parse_args(args)
1896
1897 if options.deactivate_update:
1898 RunGit(['config', 'rietveld.autoupdate', 'false'])
1899 return
1900
1901 if options.activate_update:
1902 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1903 return
1904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00001906 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907 return 0
1908
1909 url = args[0]
1910 if not url.endswith('codereview.settings'):
1911 url = os.path.join(url, 'codereview.settings')
1912
1913 # Load code review settings and download hooks (if available).
1914 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
1915 return 0
1916
1917
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001918def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001919 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001920 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1921 branch = ShortBranchName(branchref)
1922 _, args = parser.parse_args(args)
1923 if not args:
1924 print("Current base-url:")
1925 return RunGit(['config', 'branch.%s.base-url' % branch],
1926 error_ok=False).strip()
1927 else:
1928 print("Setting base-url to %s" % args[0])
1929 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1930 error_ok=False).strip()
1931
1932
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001933def color_for_status(status):
1934 """Maps a Changelist status to color, for CMDstatus and other tools."""
1935 return {
1936 'unsent': Fore.RED,
1937 'waiting': Fore.BLUE,
1938 'reply': Fore.YELLOW,
1939 'lgtm': Fore.GREEN,
1940 'commit': Fore.MAGENTA,
1941 'closed': Fore.CYAN,
1942 'error': Fore.WHITE,
1943 }.get(status, Fore.WHITE)
1944
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001945def fetch_cl_status(branch, auth_config=None):
1946 """Fetches information for an issue and returns (branch, issue, status)."""
1947 cl = Changelist(branchref=branch, auth_config=auth_config)
1948 url = cl.GetIssueURL()
1949 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001950
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001951 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001952 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001953 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001954
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001955 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001956
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001957def get_cl_statuses(
1958 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001959 """Returns a blocking iterable of (branch, issue, color) for given branches.
1960
1961 If fine_grained is true, this will fetch CL statuses from the server.
1962 Otherwise, simply indicate if there's a matching url for the given branches.
1963
1964 If max_processes is specified, it is used as the maximum number of processes
1965 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1966 spawned.
1967 """
1968 # Silence upload.py otherwise it becomes unwieldly.
1969 upload.verbosity = 0
1970
1971 if fine_grained:
1972 # Process one branch synchronously to work through authentication, then
1973 # spawn processes to process all the other branches in parallel.
1974 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001975 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1976 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001977
1978 branches_to_fetch = branches[1:]
1979 pool = ThreadPool(
1980 min(max_processes, len(branches_to_fetch))
1981 if max_processes is not None
1982 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001983 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001984 yield x
1985 else:
1986 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1987 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001988 cl = Changelist(branchref=b, auth_config=auth_config)
1989 url = cl.GetIssueURL()
1990 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001991
rmistry@google.com2dd99862015-06-22 12:22:18 +00001992
1993def upload_branch_deps(cl, args):
1994 """Uploads CLs of local branches that are dependents of the current branch.
1995
1996 If the local branch dependency tree looks like:
1997 test1 -> test2.1 -> test3.1
1998 -> test3.2
1999 -> test2.2 -> test3.3
2000
2001 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2002 run on the dependent branches in this order:
2003 test2.1, test3.1, test3.2, test2.2, test3.3
2004
2005 Note: This function does not rebase your local dependent branches. Use it when
2006 you make a change to the parent branch that will not conflict with its
2007 dependent branches, and you would like their dependencies updated in
2008 Rietveld.
2009 """
2010 if git_common.is_dirty_git_tree('upload-branch-deps'):
2011 return 1
2012
2013 root_branch = cl.GetBranch()
2014 if root_branch is None:
2015 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2016 'Get on a branch!')
2017 if not cl.GetIssue() or not cl.GetPatchset():
2018 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2019 'patchset dependencies without an uploaded CL.')
2020
2021 branches = RunGit(['for-each-ref',
2022 '--format=%(refname:short) %(upstream:short)',
2023 'refs/heads'])
2024 if not branches:
2025 print('No local branches found.')
2026 return 0
2027
2028 # Create a dictionary of all local branches to the branches that are dependent
2029 # on it.
2030 tracked_to_dependents = collections.defaultdict(list)
2031 for b in branches.splitlines():
2032 tokens = b.split()
2033 if len(tokens) == 2:
2034 branch_name, tracked = tokens
2035 tracked_to_dependents[tracked].append(branch_name)
2036
2037 print
2038 print 'The dependent local branches of %s are:' % root_branch
2039 dependents = []
2040 def traverse_dependents_preorder(branch, padding=''):
2041 dependents_to_process = tracked_to_dependents.get(branch, [])
2042 padding += ' '
2043 for dependent in dependents_to_process:
2044 print '%s%s' % (padding, dependent)
2045 dependents.append(dependent)
2046 traverse_dependents_preorder(dependent, padding)
2047 traverse_dependents_preorder(root_branch)
2048 print
2049
2050 if not dependents:
2051 print 'There are no dependent local branches for %s' % root_branch
2052 return 0
2053
2054 print ('This command will checkout all dependent branches and run '
2055 '"git cl upload".')
2056 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2057
andybons@chromium.org962f9462016-02-03 20:00:42 +00002058 # Add a default patchset title to all upload calls in Rietveld.
2059 if not settings.GetIsGerrit():
2060 args.extend(['-t', 'Updated patchset dependency'])
2061
rmistry@google.com2dd99862015-06-22 12:22:18 +00002062 # Record all dependents that failed to upload.
2063 failures = {}
2064 # Go through all dependents, checkout the branch and upload.
2065 try:
2066 for dependent_branch in dependents:
2067 print
2068 print '--------------------------------------'
2069 print 'Running "git cl upload" from %s:' % dependent_branch
2070 RunGit(['checkout', '-q', dependent_branch])
2071 print
2072 try:
2073 if CMDupload(OptionParser(), args) != 0:
2074 print 'Upload failed for %s!' % dependent_branch
2075 failures[dependent_branch] = 1
2076 except: # pylint: disable=W0702
2077 failures[dependent_branch] = 1
2078 print
2079 finally:
2080 # Swap back to the original root branch.
2081 RunGit(['checkout', '-q', root_branch])
2082
2083 print
2084 print 'Upload complete for dependent branches!'
2085 for dependent_branch in dependents:
2086 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2087 print ' %s : %s' % (dependent_branch, upload_status)
2088 print
2089
2090 return 0
2091
2092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002093def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002094 """Show status of changelists.
2095
2096 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002097 - Red not sent for review or broken
2098 - Blue waiting for review
2099 - Yellow waiting for you to reply to review
2100 - Green LGTM'ed
2101 - Magenta in the commit queue
2102 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002103
2104 Also see 'git cl comments'.
2105 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 parser.add_option('--field',
2107 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002108 parser.add_option('-f', '--fast', action='store_true',
2109 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002110 parser.add_option(
2111 '-j', '--maxjobs', action='store', type=int,
2112 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002113
2114 auth.add_auth_options(parser)
2115 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002116 if args:
2117 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002118 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002121 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 if options.field.startswith('desc'):
2123 print cl.GetDescription()
2124 elif options.field == 'id':
2125 issueid = cl.GetIssue()
2126 if issueid:
2127 print issueid
2128 elif options.field == 'patch':
2129 patchset = cl.GetPatchset()
2130 if patchset:
2131 print patchset
2132 elif options.field == 'url':
2133 url = cl.GetIssueURL()
2134 if url:
2135 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002136 return 0
2137
2138 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2139 if not branches:
2140 print('No local branch found.')
2141 return 0
2142
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002143 changes = (
2144 Changelist(branchref=b, auth_config=auth_config)
2145 for b in branches.splitlines())
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002146 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002147 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002148 alignment = max(5, max(len(b) for b in branches))
2149 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002150 output = get_cl_statuses(branches,
2151 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002152 max_processes=options.maxjobs,
2153 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002154
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002155 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002156 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002157 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002158 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002159 b, i, status = output.next()
2160 branch_statuses[b] = (i, status)
2161 issue_url, status = branch_statuses.pop(branch)
2162 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00002163 reset = Fore.RESET
2164 if not sys.stdout.isatty():
2165 color = ''
2166 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002167 status_str = '(%s)' % status if status else ''
2168 print ' %*s : %s%s %s%s' % (
2169 alignment, ShortBranchName(branch), color, issue_url, status_str,
2170 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002171
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002172 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002173 print
2174 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002175 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00002176 if not cl.GetIssue():
2177 print 'No issue assigned.'
2178 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002179 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00002180 if not options.fast:
2181 print 'Issue description:'
2182 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183 return 0
2184
2185
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002186def colorize_CMDstatus_doc():
2187 """To be called once in main() to add colors to git cl status help."""
2188 colors = [i for i in dir(Fore) if i[0].isupper()]
2189
2190 def colorize_line(line):
2191 for color in colors:
2192 if color in line.upper():
2193 # Extract whitespaces first and the leading '-'.
2194 indent = len(line) - len(line.lstrip(' ')) + 1
2195 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
2196 return line
2197
2198 lines = CMDstatus.__doc__.splitlines()
2199 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
2200
2201
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002202@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002204 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002205
2206 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002207 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00002208 parser.add_option('-r', '--reverse', action='store_true',
2209 help='Lookup the branch(es) for the specified issues. If '
2210 'no issues are specified, all branches with mapped '
2211 'issues will be listed.')
2212 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002213
dnj@chromium.org406c4402015-03-03 17:22:28 +00002214 if options.reverse:
2215 branches = RunGit(['for-each-ref', 'refs/heads',
2216 '--format=%(refname:short)']).splitlines()
2217
2218 # Reverse issue lookup.
2219 issue_branch_map = {}
2220 for branch in branches:
2221 cl = Changelist(branchref=branch)
2222 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
2223 if not args:
2224 args = sorted(issue_branch_map.iterkeys())
2225 for issue in args:
2226 if not issue:
2227 continue
2228 print 'Branch for issue number %s: %s' % (
2229 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
2230 else:
2231 cl = Changelist()
2232 if len(args) > 0:
2233 try:
2234 issue = int(args[0])
2235 except ValueError:
2236 DieWithError('Pass a number to set the issue or none to list it.\n'
2237 'Maybe you want to run git cl status?')
2238 cl.SetIssue(issue)
2239 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002240 return 0
2241
2242
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002243def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002244 """Shows or posts review comments for any changelist."""
2245 parser.add_option('-a', '--add-comment', dest='comment',
2246 help='comment to add to an issue')
2247 parser.add_option('-i', dest='issue',
2248 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00002249 parser.add_option('-j', '--json-file',
2250 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002251 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002252 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002253 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002254
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002255 issue = None
2256 if options.issue:
2257 try:
2258 issue = int(options.issue)
2259 except ValueError:
2260 DieWithError('A review issue id is expected to be a number')
2261
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002262 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002263
2264 if options.comment:
2265 cl.AddComment(options.comment)
2266 return 0
2267
2268 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00002269 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00002270 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00002271 summary.append({
2272 'date': message['date'],
2273 'lgtm': False,
2274 'message': message['text'],
2275 'not_lgtm': False,
2276 'sender': message['sender'],
2277 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002278 if message['disapproval']:
2279 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002280 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002281 elif message['approval']:
2282 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002283 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002284 elif message['sender'] == data['owner_email']:
2285 color = Fore.MAGENTA
2286 else:
2287 color = Fore.BLUE
2288 print '\n%s%s %s%s' % (
2289 color, message['date'].split('.', 1)[0], message['sender'],
2290 Fore.RESET)
2291 if message['text'].strip():
2292 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002293 if options.json_file:
2294 with open(options.json_file, 'wb') as f:
2295 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002296 return 0
2297
2298
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002299def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002300 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002301 parser.add_option('-d', '--display', action='store_true',
2302 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002303 auth.add_auth_options(parser)
2304 options, _ = parser.parse_args(args)
2305 auth_config = auth.extract_auth_config_from_options(options)
2306 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002307 if not cl.GetIssue():
2308 DieWithError('This branch has no associated changelist.')
2309 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002310 if options.display:
2311 print description.description
2312 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002313 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002314 if cl.GetDescription() != description.description:
2315 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002316 return 0
2317
2318
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002319def CreateDescriptionFromLog(args):
2320 """Pulls out the commit log to use as a base for the CL description."""
2321 log_args = []
2322 if len(args) == 1 and not args[0].endswith('.'):
2323 log_args = [args[0] + '..']
2324 elif len(args) == 1 and args[0].endswith('...'):
2325 log_args = [args[0][:-1]]
2326 elif len(args) == 2:
2327 log_args = [args[0] + '..' + args[1]]
2328 else:
2329 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002330 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002331
2332
thestig@chromium.org44202a22014-03-11 19:22:18 +00002333def CMDlint(parser, args):
2334 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002335 parser.add_option('--filter', action='append', metavar='-x,+y',
2336 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002337 auth.add_auth_options(parser)
2338 options, args = parser.parse_args(args)
2339 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002340
2341 # Access to a protected member _XX of a client class
2342 # pylint: disable=W0212
2343 try:
2344 import cpplint
2345 import cpplint_chromium
2346 except ImportError:
2347 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2348 return 1
2349
2350 # Change the current working directory before calling lint so that it
2351 # shows the correct base.
2352 previous_cwd = os.getcwd()
2353 os.chdir(settings.GetRoot())
2354 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002355 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002356 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2357 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002358 if not files:
2359 print "Cannot lint an empty CL"
2360 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002361
2362 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002363 command = args + files
2364 if options.filter:
2365 command = ['--filter=' + ','.join(options.filter)] + command
2366 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002367
2368 white_regex = re.compile(settings.GetLintRegex())
2369 black_regex = re.compile(settings.GetLintIgnoreRegex())
2370 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2371 for filename in filenames:
2372 if white_regex.match(filename):
2373 if black_regex.match(filename):
2374 print "Ignoring file %s" % filename
2375 else:
2376 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2377 extra_check_functions)
2378 else:
2379 print "Skipping file %s" % filename
2380 finally:
2381 os.chdir(previous_cwd)
2382 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2383 if cpplint._cpplint_state.error_count != 0:
2384 return 1
2385 return 0
2386
2387
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002388def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002389 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002390 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002391 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002392 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002393 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002394 auth.add_auth_options(parser)
2395 options, args = parser.parse_args(args)
2396 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002397
sbc@chromium.org71437c02015-04-09 19:29:40 +00002398 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002399 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002400 return 1
2401
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002402 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002403 if args:
2404 base_branch = args[0]
2405 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002406 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002407 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002408
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002409 cl.RunHook(
2410 committing=not options.upload,
2411 may_prompt=False,
2412 verbose=options.verbose,
2413 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002414 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002415
2416
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002417def AddChangeIdToCommitMessage(options, args):
2418 """Re-commits using the current message, assumes the commit hook is in
2419 place.
2420 """
2421 log_desc = options.message or CreateDescriptionFromLog(args)
2422 git_command = ['commit', '--amend', '-m', log_desc]
2423 RunGit(git_command)
2424 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002425 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002426 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002427 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002428 else:
2429 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2430
2431
tandrii@chromium.org65874e12016-03-04 12:03:02 +00002432def GenerateGerritChangeId(message):
2433 """Returns Ixxxxxx...xxx change id.
2434
2435 Works the same way as
2436 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
2437 but can be called on demand on all platforms.
2438
2439 The basic idea is to generate git hash of a state of the tree, original commit
2440 message, author/committer info and timestamps.
2441 """
2442 lines = []
2443 tree_hash = RunGitSilent(['write-tree'])
2444 lines.append('tree %s' % tree_hash.strip())
2445 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
2446 if code == 0:
2447 lines.append('parent %s' % parent.strip())
2448 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
2449 lines.append('author %s' % author.strip())
2450 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
2451 lines.append('committer %s' % committer.strip())
2452 lines.append('')
2453 # Note: Gerrit's commit-hook actually cleans message of some lines and
2454 # whitespace. This code is not doing this, but it clearly won't decrease
2455 # entropy.
2456 lines.append(message)
2457 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
2458 stdin='\n'.join(lines))
2459 return 'I%s' % change_hash.strip()
2460
2461
piman@chromium.org336f9122014-09-04 02:16:55 +00002462def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002463 """upload the current branch to gerrit."""
2464 # We assume the remote called "origin" is the one we want.
2465 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002466 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002467
2468 remote, remote_branch = cl.GetRemoteBranch()
2469 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2470 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002471
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002472 change_desc = ChangeDescription(
2473 options.message or CreateDescriptionFromLog(args))
2474 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002475 print "\nDescription is empty. Aborting..."
2476 return 1
2477
2478 if options.title:
2479 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002480 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002481
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002482 if options.squash:
2483 # Try to get the message from a previous upload.
2484 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002485 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002486 if not message:
2487 if not options.force:
2488 change_desc.prompt()
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002489 if not change_desc.description:
2490 print "Description is empty; aborting."
2491 return 1
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002492 message = change_desc.description
tandrii@chromium.org57d86542016-03-04 16:11:32 +00002493 change_ids = git_footers.get_footer_change_id(message)
2494 if len(change_ids) > 1:
2495 DieWithError('too many Change-Id footers in %s branch' % shadow_branch)
2496 if not change_ids:
2497 message = git_footers.add_footer_change_id(
2498 message, GenerateGerritChangeId(message))
2499 change_desc.set_description(message)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002500 change_ids = git_footers.get_footer_change_id(message)
2501 assert len(change_ids) == 1
2502
2503 change_id = change_ids[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002504
2505 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2506 if remote is '.':
2507 # If our upstream branch is local, we base our squashed commit on its
2508 # squashed version.
2509 parent = ('refs/heads/git_cl_uploads/' +
2510 scm.GIT.ShortBranchName(upstream_branch))
2511
2512 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2513 # will create additional CLs when uploading.
2514 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2515 RunGitSilent(['rev-parse', parent + ':'])):
2516 print 'Upload upstream branch ' + upstream_branch + ' first.'
2517 return 1
2518 else:
2519 parent = cl.GetCommonAncestorWithUpstream()
2520
2521 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2522 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2523 '-m', message]).strip()
2524 else:
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00002525 if not git_footers.get_footer_change_id(change_desc.description):
tandrii@chromium.org10625002016-03-04 20:03:47 +00002526 DownloadGerritHook(False)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002527 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002528 ref_to_push = 'HEAD'
2529 parent = '%s/%s' % (gerrit_remote, branch)
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002530 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002531
2532 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2533 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002534 if len(commits) > 1:
2535 print('WARNING: This will upload %d commits. Run the following command '
2536 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002537 print('git log %s..%s' % (parent, ref_to_push))
2538 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002539 'commit.')
2540 ask_for_data('About to upload; enter to confirm.')
2541
piman@chromium.org336f9122014-09-04 02:16:55 +00002542 if options.reviewers or options.tbr_owners:
2543 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002544
ukai@chromium.orge8077812012-02-03 03:41:46 +00002545 receive_options = []
2546 cc = cl.GetCCList().split(',')
2547 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002548 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002549 cc = filter(None, cc)
2550 if cc:
2551 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002552 if change_desc.get_reviewers():
2553 receive_options.extend(
2554 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002555
ukai@chromium.orge8077812012-02-03 03:41:46 +00002556 git_command = ['push']
2557 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002558 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002559 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002560 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002561 push_stdout = gclient_utils.CheckCallAndFilter(
2562 ['git'] + git_command,
2563 print_stdout=True,
2564 # Flush after every line: useful for seeing progress when running as
2565 # recipe.
2566 filter_fn=lambda _: sys.stdout.flush())
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002567
2568 if options.squash:
tandrii@chromium.orga342c922016-03-16 07:08:25 +00002569 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2570 change_numbers = [m.group(1)
2571 for m in map(regex.match, push_stdout.splitlines())
2572 if m]
2573 if len(change_numbers) != 1:
2574 DieWithError(
2575 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2576 'Change-Id: %s') % (len(change_numbers), change_id))
2577 cl.SetIssue(change_numbers[0])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002578 head = RunGit(['rev-parse', 'HEAD']).strip()
2579 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002580 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002582
wittman@chromium.org455dc922015-01-26 20:15:50 +00002583def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2584 """Computes the remote branch ref to use for the CL.
2585
2586 Args:
2587 remote (str): The git remote for the CL.
2588 remote_branch (str): The git remote branch for the CL.
2589 target_branch (str): The target branch specified by the user.
2590 pending_prefix (str): The pending prefix from the settings.
2591 """
2592 if not (remote and remote_branch):
2593 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002594
wittman@chromium.org455dc922015-01-26 20:15:50 +00002595 if target_branch:
2596 # Cannonicalize branch references to the equivalent local full symbolic
2597 # refs, which are then translated into the remote full symbolic refs
2598 # below.
2599 if '/' not in target_branch:
2600 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2601 else:
2602 prefix_replacements = (
2603 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2604 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2605 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2606 )
2607 match = None
2608 for regex, replacement in prefix_replacements:
2609 match = re.search(regex, target_branch)
2610 if match:
2611 remote_branch = target_branch.replace(match.group(0), replacement)
2612 break
2613 if not match:
2614 # This is a branch path but not one we recognize; use as-is.
2615 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002616 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2617 # Handle the refs that need to land in different refs.
2618 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002619
wittman@chromium.org455dc922015-01-26 20:15:50 +00002620 # Create the true path to the remote branch.
2621 # Does the following translation:
2622 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2623 # * refs/remotes/origin/master -> refs/heads/master
2624 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2625 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2626 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2627 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2628 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2629 'refs/heads/')
2630 elif remote_branch.startswith('refs/remotes/branch-heads'):
2631 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2632 # If a pending prefix exists then replace refs/ with it.
2633 if pending_prefix:
2634 remote_branch = remote_branch.replace('refs/', pending_prefix)
2635 return remote_branch
2636
2637
piman@chromium.org336f9122014-09-04 02:16:55 +00002638def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002639 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640 upload_args = ['--assume_yes'] # Don't ask about untracked files.
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002641 upload_args.extend(['--server', cl.GetCodereviewServer()])
2642 # TODO(tandrii): refactor this ugliness into _RietveldChangelistImpl.
2643 upload_args.extend(auth.auth_config_to_command_options(
2644 cl._codereview_impl.GetAuthConfig()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002645 if options.emulate_svn_auto_props:
2646 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002647
2648 change_desc = None
2649
pgervais@chromium.org91141372014-01-09 23:27:20 +00002650 if options.email is not None:
2651 upload_args.extend(['--email', options.email])
2652
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002653 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002654 if options.title:
2655 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002656 if options.message:
2657 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002658 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002659 print ("This branch is associated with issue %s. "
2660 "Adding patch to that issue." % cl.GetIssue())
2661 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002662 if options.title:
2663 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002664 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002665 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002666 if options.reviewers or options.tbr_owners:
2667 change_desc.update_reviewers(options.reviewers,
2668 options.tbr_owners,
2669 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002670 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002671 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002672
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002673 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002674 print "Description is empty; aborting."
2675 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002676
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002677 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002678 if change_desc.get_reviewers():
2679 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002680 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002681 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002682 DieWithError("Must specify reviewers to send email.")
2683 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002684
2685 # We check this before applying rietveld.private assuming that in
2686 # rietveld.cc only addresses which we can send private CLs to are listed
2687 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2688 # --private is specified explicitly on the command line.
2689 if options.private:
2690 logging.warn('rietveld.cc is ignored since private flag is specified. '
2691 'You need to review and add them manually if necessary.')
2692 cc = cl.GetCCListWithoutDefault()
2693 else:
2694 cc = cl.GetCCList()
2695 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002696 if cc:
2697 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002698
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002699 if options.private or settings.GetDefaultPrivateFlag() == "True":
2700 upload_args.append('--private')
2701
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002702 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002703 if not options.find_copies:
2704 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002706 # Include the upstream repo's URL in the change -- this is useful for
2707 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002708 remote_url = cl.GetGitBaseUrlFromConfig()
2709 if not remote_url:
2710 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002711 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002712 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002713 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2714 remote_url = (cl.GetRemoteUrl() + '@'
2715 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002716 if remote_url:
2717 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002718 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002719 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2720 settings.GetPendingRefPrefix())
2721 if target_ref:
2722 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002723
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002724 # Look for dependent patchsets. See crbug.com/480453 for more details.
2725 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2726 upstream_branch = ShortBranchName(upstream_branch)
2727 if remote is '.':
2728 # A local branch is being tracked.
2729 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002730 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002731 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002732 print ('Skipping dependency patchset upload because git config '
2733 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002734 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002735 else:
2736 auth_config = auth.extract_auth_config_from_options(options)
2737 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2738 branch_cl_issue_url = branch_cl.GetIssueURL()
2739 branch_cl_issue = branch_cl.GetIssue()
2740 branch_cl_patchset = branch_cl.GetPatchset()
2741 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2742 upload_args.extend(
2743 ['--depends_on_patchset', '%s:%s' % (
2744 branch_cl_issue, branch_cl_patchset)])
2745 print
2746 print ('The current branch (%s) is tracking a local branch (%s) with '
2747 'an associated CL.') % (cl.GetBranch(), local_branch)
2748 print 'Adding %s/#ps%s as a dependency patchset.' % (
2749 branch_cl_issue_url, branch_cl_patchset)
2750 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002751
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002752 project = settings.GetProject()
2753 if project:
2754 upload_args.extend(['--project', project])
2755
rmistry@google.comef966222015-04-07 11:15:01 +00002756 if options.cq_dry_run:
2757 upload_args.extend(['--cq_dry_run'])
2758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002759 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002760 upload_args = ['upload'] + upload_args + args
2761 logging.info('upload.RealMain(%s)', upload_args)
2762 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002763 issue = int(issue)
2764 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002765 except KeyboardInterrupt:
2766 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002767 except:
2768 # If we got an exception after the user typed a description for their
2769 # change, back up the description before re-raising.
2770 if change_desc:
2771 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2772 print '\nGot exception while uploading -- saving description to %s\n' \
2773 % backup_path
2774 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002775 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002776 backup_file.close()
2777 raise
2778
2779 if not cl.GetIssue():
2780 cl.SetIssue(issue)
2781 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002782
2783 if options.use_commit_queue:
2784 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002785 return 0
2786
2787
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002788def cleanup_list(l):
2789 """Fixes a list so that comma separated items are put as individual items.
2790
2791 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2792 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2793 """
2794 items = sum((i.split(',') for i in l), [])
2795 stripped_items = (i.strip() for i in items)
2796 return sorted(filter(None, stripped_items))
2797
2798
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002799@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002800def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002801 """Uploads the current changelist to codereview.
2802
2803 Can skip dependency patchset uploads for a branch by running:
2804 git config branch.branch_name.skip-deps-uploads True
2805 To unset run:
2806 git config --unset branch.branch_name.skip-deps-uploads
2807 Can also set the above globally by using the --global flag.
2808 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002809 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2810 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002811 parser.add_option('--bypass-watchlists', action='store_true',
2812 dest='bypass_watchlists',
2813 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002814 parser.add_option('-f', action='store_true', dest='force',
2815 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002816 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002817 parser.add_option('-t', dest='title',
2818 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002819 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002820 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002821 help='reviewer email addresses')
2822 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002823 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002824 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002825 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002826 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002827 parser.add_option('--emulate_svn_auto_props',
2828 '--emulate-svn-auto-props',
2829 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002830 dest="emulate_svn_auto_props",
2831 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002832 parser.add_option('-c', '--use-commit-queue', action='store_true',
2833 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002834 parser.add_option('--private', action='store_true',
2835 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002836 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002837 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002838 metavar='TARGET',
2839 help='Apply CL to remote ref TARGET. ' +
2840 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002841 parser.add_option('--squash', action='store_true',
2842 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002843 parser.add_option('--no-squash', action='store_true',
2844 help='Don\'t squash multiple commits into one ' +
2845 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002846 parser.add_option('--email', default=None,
2847 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002848 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2849 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002850 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2851 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002852 help='Send the patchset to do a CQ dry run right after '
2853 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002854 parser.add_option('--dependencies', action='store_true',
2855 help='Uploads CLs of all the local branches that depend on '
2856 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002857
rmistry@google.com2dd99862015-06-22 12:22:18 +00002858 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002859 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002860 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002861 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002862 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002863
sbc@chromium.org71437c02015-04-09 19:29:40 +00002864 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002865 return 1
2866
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002867 options.reviewers = cleanup_list(options.reviewers)
2868 options.cc = cleanup_list(options.cc)
2869
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002870 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002871 if args:
2872 # TODO(ukai): is it ok for gerrit case?
2873 base_branch = args[0]
2874 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002875 if cl.GetBranch() is None:
2876 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2877
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002878 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002879 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002880 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002881
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002882 # Make sure authenticated to Rietveld before running expensive hooks. It is
2883 # a fast, best efforts check. Rietveld still can reject the authentication
2884 # during the actual upload.
2885 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2886 authenticator = auth.get_authenticator_for_host(
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002887 cl.GetCodereviewServer(), auth_config)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002888 if not authenticator.has_cached_credentials():
tandrii@chromium.orge044c812016-03-24 10:13:29 +00002889 raise auth.LoginRequiredError(cl.GetCodereviewServer())
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002890
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002891 # Apply watchlists on upload.
2892 change = cl.GetChange(base_branch, None)
2893 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2894 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002895 if not options.bypass_watchlists:
2896 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002897
ukai@chromium.orge8077812012-02-03 03:41:46 +00002898 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002899 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002900 # Set the reviewer list now so that presubmit checks can access it.
2901 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002902 change_description.update_reviewers(options.reviewers,
2903 options.tbr_owners,
2904 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002905 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002906 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002907 may_prompt=not options.force,
2908 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002909 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002910 if not hook_results.should_continue():
2911 return 1
2912 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002913 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002914
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002915 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002916 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002917 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002918 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002919 print ('The last upload made from this repository was patchset #%d but '
2920 'the most recent patchset on the server is #%d.'
2921 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002922 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2923 'from another machine or branch the patch you\'re uploading now '
2924 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002925 ask_for_data('About to upload; enter to confirm.')
2926
iannucci@chromium.org79540052012-10-19 23:15:26 +00002927 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002928 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002929 if options.squash and options.no_squash:
2930 DieWithError('Can only use one of --squash or --no-squash')
2931
2932 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2933 not options.no_squash)
2934
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002935 ret = GerritUpload(options, args, cl, change)
2936 else:
2937 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002938 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002939 git_set_branch_value('last-upload-hash',
2940 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002941 # Run post upload hooks, if specified.
2942 if settings.GetRunPostUploadHook():
2943 presubmit_support.DoPostUploadExecuter(
2944 change,
2945 cl,
2946 settings.GetRoot(),
2947 options.verbose,
2948 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002949
rmistry@google.com2dd99862015-06-22 12:22:18 +00002950 # Upload all dependencies if specified.
2951 if options.dependencies:
2952 print
2953 print '--dependencies has been specified.'
2954 print 'All dependent local branches will be re-uploaded.'
2955 print
2956 # Remove the dependencies flag from args so that we do not end up in a
2957 # loop.
2958 orig_args.remove('--dependencies')
2959 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002960 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002961
2962
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002963def IsSubmoduleMergeCommit(ref):
2964 # When submodules are added to the repo, we expect there to be a single
2965 # non-git-svn merge commit at remote HEAD with a signature comment.
2966 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002967 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002968 return RunGit(cmd) != ''
2969
2970
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002971def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002972 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002974 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002975 Updates changelog with metadata (e.g. pointer to review).
2976 Pushes/dcommits the code upstream.
2977 Updates review and closes.
2978 """
2979 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2980 help='bypass upload presubmit hook')
2981 parser.add_option('-m', dest='message',
2982 help="override review description")
2983 parser.add_option('-f', action='store_true', dest='force',
2984 help="force yes to questions (don't prompt)")
2985 parser.add_option('-c', dest='contributor',
2986 help="external contributor for patch (appended to " +
2987 "description and used as author for git). Should be " +
2988 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002989 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002990 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002992 auth_config = auth.extract_auth_config_from_options(options)
2993
2994 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002995
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002996 current = cl.GetBranch()
2997 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2998 if not settings.GetIsGitSvn() and remote == '.':
2999 print
3000 print 'Attempting to push branch %r into another local branch!' % current
3001 print
3002 print 'Either reparent this branch on top of origin/master:'
3003 print ' git reparent-branch --root'
3004 print
3005 print 'OR run `git rebase-update` if you think the parent branch is already'
3006 print 'committed.'
3007 print
3008 print ' Current parent: %r' % upstream_branch
3009 return 1
3010
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003011 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003012 # Default to merging against our best guess of the upstream branch.
3013 args = [cl.GetUpstreamBranch()]
3014
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003015 if options.contributor:
3016 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3017 print "Please provide contibutor as 'First Last <email@example.com>'"
3018 return 1
3019
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003020 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003021 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003022
sbc@chromium.org71437c02015-04-09 19:29:40 +00003023 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003024 return 1
3025
3026 # This rev-list syntax means "show all commits not in my branch that
3027 # are in base_branch".
3028 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3029 base_branch]).splitlines()
3030 if upstream_commits:
3031 print ('Base branch "%s" has %d commits '
3032 'not in this branch.' % (base_branch, len(upstream_commits)))
3033 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3034 return 1
3035
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003036 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003037 svn_head = None
3038 if cmd == 'dcommit' or base_has_submodules:
3039 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3040 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003043 # If the base_head is a submodule merge commit, the first parent of the
3044 # base_head should be a git-svn commit, which is what we're interested in.
3045 base_svn_head = base_branch
3046 if base_has_submodules:
3047 base_svn_head += '^1'
3048
3049 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003050 if extra_commits:
3051 print ('This branch has %d additional commits not upstreamed yet.'
3052 % len(extra_commits.splitlines()))
3053 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3054 'before attempting to %s.' % (base_branch, cmd))
3055 return 1
3056
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003057 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003058 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003059 author = None
3060 if options.contributor:
3061 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003062 hook_results = cl.RunHook(
3063 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003064 may_prompt=not options.force,
3065 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003066 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003067 if not hook_results.should_continue():
3068 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003069
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003070 # Check the tree status if the tree status URL is set.
3071 status = GetTreeStatus()
3072 if 'closed' == status:
3073 print('The tree is closed. Please wait for it to reopen. Use '
3074 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3075 return 1
3076 elif 'unknown' == status:
3077 print('Unable to determine tree status. Please verify manually and '
3078 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3079 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003080
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003081 change_desc = ChangeDescription(options.message)
3082 if not change_desc.description and cl.GetIssue():
3083 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003084
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003085 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003086 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003087 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003088 else:
3089 print 'No description set.'
3090 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3091 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003092
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003093 # Keep a separate copy for the commit message, because the commit message
3094 # contains the link to the Rietveld issue, while the Rietveld message contains
3095 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003096 # Keep a separate copy for the commit message.
3097 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003098 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003099
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003100 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003101 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003102 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003103 # after it. Add a period on a new line to circumvent this. Also add a space
3104 # before the period to make sure that Gitiles continues to correctly resolve
3105 # the URL.
3106 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003107 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003108 commit_desc.append_footer('Patch from %s.' % options.contributor)
3109
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003110 print('Description:')
3111 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003112
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003113 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003114 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003115 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003116
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003117 # We want to squash all this branch's commits into one commit with the proper
3118 # description. We do this by doing a "reset --soft" to the base branch (which
3119 # keeps the working copy the same), then dcommitting that. If origin/master
3120 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3121 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003122 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003123 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3124 # Delete the branches if they exist.
3125 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3126 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3127 result = RunGitWithCode(showref_cmd)
3128 if result[0] == 0:
3129 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003130
3131 # We might be in a directory that's present in this branch but not in the
3132 # trunk. Move up to the top of the tree so that git commands that expect a
3133 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003134 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003135 if rel_base_path:
3136 os.chdir(rel_base_path)
3137
3138 # Stuff our change into the merge branch.
3139 # We wrap in a try...finally block so if anything goes wrong,
3140 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003141 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003142 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003143 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003144 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003145 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003146 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003147 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003148 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003149 RunGit(
3150 [
3151 'commit', '--author', options.contributor,
3152 '-m', commit_desc.description,
3153 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003154 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003155 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003156 if base_has_submodules:
3157 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3158 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3159 RunGit(['checkout', CHERRY_PICK_BRANCH])
3160 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003161 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003162 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003163 mirror = settings.GetGitMirror(remote)
3164 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003165 pending_prefix = settings.GetPendingRefPrefix()
3166 if not pending_prefix or branch.startswith(pending_prefix):
3167 # If not using refs/pending/heads/* at all, or target ref is already set
3168 # to pending, then push to the target ref directly.
3169 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003170 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003171 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003172 else:
3173 # Cherry-pick the change on top of pending ref and then push it.
3174 assert branch.startswith('refs/'), branch
3175 assert pending_prefix[-1] == '/', pending_prefix
3176 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003177 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003178 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003179 if retcode == 0:
3180 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 else:
3182 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003183 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003184 'svn', 'dcommit',
3185 '-C%s' % options.similarity,
3186 '--no-rebase', '--rmdir',
3187 ]
3188 if settings.GetForceHttpsCommitUrl():
3189 # Allow forcing https commit URLs for some projects that don't allow
3190 # committing to http URLs (like Google Code).
3191 remote_url = cl.GetGitSvnRemoteUrl()
3192 if urlparse.urlparse(remote_url).scheme == 'http':
3193 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003194 cmd_args.append('--commit-url=%s' % remote_url)
3195 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003196 if 'Committed r' in output:
3197 revision = re.match(
3198 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3199 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003200 finally:
3201 # And then swap back to the original branch and clean up.
3202 RunGit(['checkout', '-q', cl.GetBranch()])
3203 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003204 if base_has_submodules:
3205 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003206
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003207 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003208 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003209 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003210
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003211 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003212 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003213 try:
3214 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3215 # We set pushed_to_pending to False, since it made it all the way to the
3216 # real ref.
3217 pushed_to_pending = False
3218 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003219 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003221 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003222 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003223 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003224 if not to_pending:
3225 if viewvc_url and revision:
3226 change_desc.append_footer(
3227 'Committed: %s%s' % (viewvc_url, revision))
3228 elif revision:
3229 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003230 print ('Closing issue '
3231 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003232 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003233 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003234 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003235 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003236 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003237 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003238 if options.bypass_hooks:
3239 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3240 else:
3241 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003242 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003243 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003244
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003245 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003246 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3247 print 'The commit is in the pending queue (%s).' % pending_ref
3248 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003249 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003250 'footer.' % branch)
3251
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003252 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3253 if os.path.isfile(hook):
3254 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003255
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003256 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257
3258
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003259def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3260 print
3261 print 'Waiting for commit to be landed on %s...' % real_ref
3262 print '(If you are impatient, you may Ctrl-C once without harm)'
3263 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3264 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003265 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003266
3267 loop = 0
3268 while True:
3269 sys.stdout.write('fetching (%d)... \r' % loop)
3270 sys.stdout.flush()
3271 loop += 1
3272
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003273 if mirror:
3274 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003275 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3276 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3277 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3278 for commit in commits.splitlines():
3279 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3280 print 'Found commit on %s' % real_ref
3281 return commit
3282
3283 current_rev = to_rev
3284
3285
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003286def PushToGitPending(remote, pending_ref, upstream_ref):
3287 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3288
3289 Returns:
3290 (retcode of last operation, output log of last operation).
3291 """
3292 assert pending_ref.startswith('refs/'), pending_ref
3293 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3294 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3295 code = 0
3296 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003297 max_attempts = 3
3298 attempts_left = max_attempts
3299 while attempts_left:
3300 if attempts_left != max_attempts:
3301 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3302 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003303
3304 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003305 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003306 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003307 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003308 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003309 print 'Fetch failed with exit code %d.' % code
3310 if out.strip():
3311 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003312 continue
3313
3314 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003315 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003316 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003317 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003318 if code:
3319 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003320 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3321 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003322 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3323 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003324 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003325 return code, out
3326
3327 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003328 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003329 code, out = RunGitWithCode(
3330 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3331 if code == 0:
3332 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003333 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003334 return code, out
3335
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003336 print 'Push failed with exit code %d.' % code
3337 if out.strip():
3338 print out.strip()
3339 if IsFatalPushFailure(out):
3340 print (
3341 'Fatal push error. Make sure your .netrc credentials and git '
3342 'user.email are correct and you have push access to the repo.')
3343 return code, out
3344
3345 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003346 return code, out
3347
3348
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003349def IsFatalPushFailure(push_stdout):
3350 """True if retrying push won't help."""
3351 return '(prohibited by Gerrit)' in push_stdout
3352
3353
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003354@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003355def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003356 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003357 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003358 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003359 # If it looks like previous commits were mirrored with git-svn.
3360 message = """This repository appears to be a git-svn mirror, but no
3361upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3362 else:
3363 message = """This doesn't appear to be an SVN repository.
3364If your project has a true, writeable git repository, you probably want to run
3365'git cl land' instead.
3366If your project has a git mirror of an upstream SVN master, you probably need
3367to run 'git svn init'.
3368
3369Using the wrong command might cause your commit to appear to succeed, and the
3370review to be closed, without actually landing upstream. If you choose to
3371proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003372 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003373 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003374 return SendUpstream(parser, args, 'dcommit')
3375
3376
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003377@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003378def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003379 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003380 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381 print('This appears to be an SVN repository.')
3382 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003383 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003384 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003385 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003386
3387
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003388def ParseIssueNum(arg):
3389 """Parses the issue number from args if present otherwise returns None."""
3390 if re.match(r'\d+', arg):
3391 return arg
3392 if arg.startswith('http'):
3393 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3394 return None
3395
3396
3397@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003399 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003400 parser.add_option('-b', dest='newbranch',
3401 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003402 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003403 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003404 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3405 help='Change to the directory DIR immediately, '
3406 'before doing anything else.')
3407 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003408 help='failed patches spew .rej files rather than '
3409 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003410 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3411 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003412
3413 group = optparse.OptionGroup(parser,
3414 """Options for continuing work on the current issue uploaded
3415from a different clone (e.g. different machine). Must be used independently from
3416the other options. No issue number should be specified, and the branch must have
3417an issue number associated with it""")
3418 group.add_option('--reapply', action='store_true',
3419 dest='reapply',
3420 help="""Reset the branch and reapply the issue.
3421CAUTION: This will undo any local changes in this branch""")
3422
3423 group.add_option('--pull', action='store_true', dest='pull',
3424 help="Performs a pull before reapplying.")
3425 parser.add_option_group(group)
3426
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003427 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003428 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003429 auth_config = auth.extract_auth_config_from_options(options)
3430
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003431 issue_arg = None
3432 if options.reapply :
3433 if len(args) > 0:
3434 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003435
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003436 cl = Changelist()
3437 issue_arg = cl.GetIssue()
3438 upstream = cl.GetUpstreamBranch()
3439 if upstream == None:
3440 parser.error("No upstream branch specified. Cannot reset branch")
3441
3442 RunGit(['reset', '--hard', upstream])
3443 if options.pull:
3444 RunGit(['pull'])
3445 else:
3446 if len(args) != 1:
3447 parser.error("Must specify issue number")
3448
3449 issue_arg = ParseIssueNum(args[0])
3450
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003451 # The patch URL works because ParseIssueNum won't do any substitution
3452 # as the re.sub pattern fails to match and just returns it.
3453 if issue_arg == None:
3454 parser.print_help()
3455 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003456
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003457 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003458 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003459 return 1
3460
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003461 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003462 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003463
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003464 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003465 if options.reapply:
3466 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003467 if options.force:
3468 RunGit(['branch', '-D', options.newbranch],
3469 stderr=subprocess2.PIPE, error_ok=True)
3470 RunGit(['checkout', '-b', options.newbranch,
3471 Changelist().GetUpstreamBranch()])
3472
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003473 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003474 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003475
3476
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003477def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003478 # PatchIssue should never be called with a dirty tree. It is up to the
3479 # caller to check this, but just in case we assert here since the
3480 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003481 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003482
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003483 # TODO(tandrii): implement for Gerrit.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003484 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003485 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003486 issue = int(issue_arg)
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003487 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003488 patchset = cl.GetMostRecentPatchset()
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003489 patch_data = cl._codereview_impl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003490 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003491 # Assume it's a URL to the patch. Default to https.
3492 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003493 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003494 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003495 DieWithError('Must pass an issue ID or full URL for '
3496 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003497 issue = int(match.group(2))
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003498 cl = Changelist(issue=issue, codereview='rietveld',
3499 rietvled_server=match.group(1), auth_config=auth_config)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003500 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003501 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003502
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003503 # Switch up to the top-level directory, if necessary, in preparation for
3504 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003505 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003506 if top:
3507 os.chdir(top)
3508
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003509 # Git patches have a/ at the beginning of source paths. We strip that out
3510 # with a sed script rather than the -p flag to patch so we can feed either
3511 # Git or svn-style patches into the same apply command.
3512 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003513 try:
3514 patch_data = subprocess2.check_output(
3515 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3516 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003517 DieWithError('Git patch mungling failed.')
3518 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003519
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003520 # We use "git apply" to apply the patch instead of "patch" so that we can
3521 # pick up file adds.
3522 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003523 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003524 if directory:
3525 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003526 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003527 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003528 elif IsGitVersionAtLeast('1.7.12'):
3529 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003530 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003531 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003532 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003533 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003534 print 'Failed to apply the patch'
3535 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003536
3537 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003538 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003539 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3540 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003541 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3542 % {'i': issue, 'p': patchset})])
tandrii@chromium.orge044c812016-03-24 10:13:29 +00003543 cl = Changelist(codereview='rietveld', auth_config=auth_config,
3544 rietveld_server=cl.GetCodereviewServer())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003546 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003547 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548 else:
3549 print "Patch applied to index."
3550 return 0
3551
3552
3553def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003554 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003555 # Provide a wrapper for git svn rebase to help avoid accidental
3556 # git svn dcommit.
3557 # It's the only command that doesn't use parser at all since we just defer
3558 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003559
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003560 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561
3562
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003563def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564 """Fetches the tree status and returns either 'open', 'closed',
3565 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003566 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003567 if url:
3568 status = urllib2.urlopen(url).read().lower()
3569 if status.find('closed') != -1 or status == '0':
3570 return 'closed'
3571 elif status.find('open') != -1 or status == '1':
3572 return 'open'
3573 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003574 return 'unset'
3575
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003576
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003577def GetTreeStatusReason():
3578 """Fetches the tree status from a json url and returns the message
3579 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003580 url = settings.GetTreeStatusUrl()
3581 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003582 connection = urllib2.urlopen(json_url)
3583 status = json.loads(connection.read())
3584 connection.close()
3585 return status['message']
3586
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003587
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003588def GetBuilderMaster(bot_list):
3589 """For a given builder, fetch the master from AE if available."""
3590 map_url = 'https://builders-map.appspot.com/'
3591 try:
3592 master_map = json.load(urllib2.urlopen(map_url))
3593 except urllib2.URLError as e:
3594 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3595 (map_url, e))
3596 except ValueError as e:
3597 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3598 if not master_map:
3599 return None, 'Failed to build master map.'
3600
3601 result_master = ''
3602 for bot in bot_list:
3603 builder = bot.split(':', 1)[0]
3604 master_list = master_map.get(builder, [])
3605 if not master_list:
3606 return None, ('No matching master for builder %s.' % builder)
3607 elif len(master_list) > 1:
3608 return None, ('The builder name %s exists in multiple masters %s.' %
3609 (builder, master_list))
3610 else:
3611 cur_master = master_list[0]
3612 if not result_master:
3613 result_master = cur_master
3614 elif result_master != cur_master:
3615 return None, 'The builders do not belong to the same master.'
3616 return result_master, None
3617
3618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003620 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003621 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003622 status = GetTreeStatus()
3623 if 'unset' == status:
3624 print 'You must configure your tree status URL by running "git cl config".'
3625 return 2
3626
3627 print "The tree is %s" % status
3628 print
3629 print GetTreeStatusReason()
3630 if status != 'open':
3631 return 1
3632 return 0
3633
3634
maruel@chromium.org15192402012-09-06 12:38:29 +00003635def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003636 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003637 group = optparse.OptionGroup(parser, "Try job options")
3638 group.add_option(
3639 "-b", "--bot", action="append",
3640 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3641 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003642 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003643 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003644 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003645 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003646 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003647 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003648 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003649 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003650 "-r", "--revision",
3651 help="Revision to use for the try job; default: the "
3652 "revision will be determined by the try server; see "
3653 "its waterfall for more info")
3654 group.add_option(
3655 "-c", "--clobber", action="store_true", default=False,
3656 help="Force a clobber before building; e.g. don't do an "
3657 "incremental build")
3658 group.add_option(
3659 "--project",
3660 help="Override which project to use. Projects are defined "
3661 "server-side to define what default bot set to use")
3662 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003663 "-p", "--property", dest="properties", action="append", default=[],
3664 help="Specify generic properties in the form -p key1=value1 -p "
3665 "key2=value2 etc (buildbucket only). The value will be treated as "
3666 "json if decodable, or as string otherwise.")
3667 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003668 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003669 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003670 "--use-rietveld", action="store_true", default=False,
3671 help="Use Rietveld to trigger try jobs.")
3672 group.add_option(
3673 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3674 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003675 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003676 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003677 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003678 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003679
machenbach@chromium.org45453142015-09-15 08:45:22 +00003680 if options.use_rietveld and options.properties:
3681 parser.error('Properties can only be specified with buildbucket')
3682
3683 # Make sure that all properties are prop=value pairs.
3684 bad_params = [x for x in options.properties if '=' not in x]
3685 if bad_params:
3686 parser.error('Got properties with missing "=": %s' % bad_params)
3687
maruel@chromium.org15192402012-09-06 12:38:29 +00003688 if args:
3689 parser.error('Unknown arguments: %s' % args)
3690
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003691 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003692 if not cl.GetIssue():
3693 parser.error('Need to upload first')
3694
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003695 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003696 if props.get('closed'):
3697 parser.error('Cannot send tryjobs for a closed CL')
3698
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003699 if props.get('private'):
3700 parser.error('Cannot use trybots with private issue')
3701
maruel@chromium.org15192402012-09-06 12:38:29 +00003702 if not options.name:
3703 options.name = cl.GetBranch()
3704
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003705 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003706 options.master, err_msg = GetBuilderMaster(options.bot)
3707 if err_msg:
3708 parser.error('Tryserver master cannot be found because: %s\n'
3709 'Please manually specify the tryserver master'
3710 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003711
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003712 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003713 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003714 if not options.bot:
3715 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003716
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003717 # Get try masters from PRESUBMIT.py files.
3718 masters = presubmit_support.DoGetTryMasters(
3719 change,
3720 change.LocalPaths(),
3721 settings.GetRoot(),
3722 None,
3723 None,
3724 options.verbose,
3725 sys.stdout)
3726 if masters:
3727 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003728
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003729 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3730 options.bot = presubmit_support.DoGetTrySlaves(
3731 change,
3732 change.LocalPaths(),
3733 settings.GetRoot(),
3734 None,
3735 None,
3736 options.verbose,
3737 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003738
3739 if not options.bot:
3740 # Get try masters from cq.cfg if any.
3741 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3742 # location.
3743 cq_cfg = os.path.join(change.RepositoryRoot(),
3744 'infra', 'config', 'cq.cfg')
3745 if os.path.exists(cq_cfg):
3746 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003747 cq_masters = commit_queue.get_master_builder_map(
3748 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003749 for master, builders in cq_masters.iteritems():
3750 for builder in builders:
3751 # Skip presubmit builders, because these will fail without LGTM.
3752 if 'presubmit' not in builder.lower():
3753 masters.setdefault(master, {})[builder] = ['defaulttests']
3754 if masters:
3755 return masters
3756
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003757 if not options.bot:
3758 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003759
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003760 builders_and_tests = {}
3761 # TODO(machenbach): The old style command-line options don't support
3762 # multiple try masters yet.
3763 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3764 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3765
3766 for bot in old_style:
3767 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003768 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003769 elif ',' in bot:
3770 parser.error('Specify one bot per --bot flag')
3771 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003772 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003773
3774 for bot, tests in new_style:
3775 builders_and_tests.setdefault(bot, []).extend(tests)
3776
3777 # Return a master map with one master to be backwards compatible. The
3778 # master name defaults to an empty string, which will cause the master
3779 # not to be set on rietveld (deprecated).
3780 return {options.master: builders_and_tests}
3781
3782 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003783
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003784 for builders in masters.itervalues():
3785 if any('triggered' in b for b in builders):
3786 print >> sys.stderr, (
3787 'ERROR You are trying to send a job to a triggered bot. This type of'
3788 ' bot requires an\ninitial job from a parent (usually a builder). '
3789 'Instead send your job to the parent.\n'
3790 'Bot list: %s' % builders)
3791 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003792
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003793 patchset = cl.GetMostRecentPatchset()
3794 if patchset and patchset != cl.GetPatchset():
3795 print(
3796 '\nWARNING Mismatch between local config and server. Did a previous '
3797 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3798 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003799 if options.luci:
3800 trigger_luci_job(cl, masters, options)
3801 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003802 try:
3803 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3804 except BuildbucketResponseException as ex:
3805 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003806 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003807 except Exception as e:
3808 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3809 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3810 e, stacktrace)
3811 return 1
3812 else:
3813 try:
3814 cl.RpcServer().trigger_distributed_try_jobs(
3815 cl.GetIssue(), patchset, options.name, options.clobber,
3816 options.revision, masters)
3817 except urllib2.HTTPError as e:
3818 if e.code == 404:
3819 print('404 from rietveld; '
3820 'did you mean to use "git try" instead of "git cl try"?')
3821 return 1
3822 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003823
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003824 for (master, builders) in sorted(masters.iteritems()):
3825 if master:
3826 print 'Master: %s' % master
3827 length = max(len(builder) for builder in builders)
3828 for builder in sorted(builders):
3829 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003830 return 0
3831
3832
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003833def CMDtry_results(parser, args):
3834 group = optparse.OptionGroup(parser, "Try job results options")
3835 group.add_option(
3836 "-p", "--patchset", type=int, help="patchset number if not current.")
3837 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00003838 "--print-master", action='store_true', help="print master name as well.")
3839 group.add_option(
3840 "--color", action='store_true', default=sys.stdout.isatty(),
3841 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003842 group.add_option(
3843 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3844 help="Host of buildbucket. The default host is %default.")
3845 parser.add_option_group(group)
3846 auth.add_auth_options(parser)
3847 options, args = parser.parse_args(args)
3848 if args:
3849 parser.error('Unrecognized args: %s' % ' '.join(args))
3850
3851 auth_config = auth.extract_auth_config_from_options(options)
3852 cl = Changelist(auth_config=auth_config)
3853 if not cl.GetIssue():
3854 parser.error('Need to upload first')
3855
3856 if not options.patchset:
3857 options.patchset = cl.GetMostRecentPatchset()
3858 if options.patchset and options.patchset != cl.GetPatchset():
3859 print(
3860 '\nWARNING Mismatch between local config and server. Did a previous '
3861 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3862 'Continuing using\npatchset %s.\n' % options.patchset)
3863 try:
3864 jobs = fetch_try_jobs(auth_config, cl, options)
3865 except BuildbucketResponseException as ex:
3866 print 'Buildbucket error: %s' % ex
3867 return 1
3868 except Exception as e:
3869 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3870 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3871 e, stacktrace)
3872 return 1
3873 print_tryjobs(options, jobs)
3874 return 0
3875
3876
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003877@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003878def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003879 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003880 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003881 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003882 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003884 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003885 if args:
3886 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003887 branch = cl.GetBranch()
3888 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003889 cl = Changelist()
3890 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003891
3892 # Clear configured merge-base, if there is one.
3893 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003894 else:
3895 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003896 return 0
3897
3898
thestig@chromium.org00858c82013-12-02 23:08:03 +00003899def CMDweb(parser, args):
3900 """Opens the current CL in the web browser."""
3901 _, args = parser.parse_args(args)
3902 if args:
3903 parser.error('Unrecognized args: %s' % ' '.join(args))
3904
3905 issue_url = Changelist().GetIssueURL()
3906 if not issue_url:
3907 print >> sys.stderr, 'ERROR No issue to open'
3908 return 1
3909
3910 webbrowser.open(issue_url)
3911 return 0
3912
3913
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003914def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003915 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003916 auth.add_auth_options(parser)
3917 options, args = parser.parse_args(args)
3918 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003919 if args:
3920 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003921 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003922 props = cl.GetIssueProperties()
3923 if props.get('private'):
3924 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003925 cl.SetFlag('commit', '1')
3926 return 0
3927
3928
groby@chromium.org411034a2013-02-26 15:12:01 +00003929def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003930 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003931 auth.add_auth_options(parser)
3932 options, args = parser.parse_args(args)
3933 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003934 if args:
3935 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003936 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003937 # Ensure there actually is an issue to close.
3938 cl.GetDescription()
3939 cl.CloseIssue()
3940 return 0
3941
3942
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003943def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003944 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003945 auth.add_auth_options(parser)
3946 options, args = parser.parse_args(args)
3947 auth_config = auth.extract_auth_config_from_options(options)
3948 if args:
3949 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003950
3951 # Uncommitted (staged and unstaged) changes will be destroyed by
3952 # "git reset --hard" if there are merging conflicts in PatchIssue().
3953 # Staged changes would be committed along with the patch from last
3954 # upload, hence counted toward the "last upload" side in the final
3955 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003956 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003957 return 1
3958
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003959 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003960 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003961 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003962 if not issue:
3963 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003964 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003965 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003966
3967 # Create a new branch based on the merge-base
3968 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3969 try:
3970 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003971 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003972 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003973 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003974 return rtn
3975
wychen@chromium.org06928532015-02-03 02:11:29 +00003976 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003977 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003978 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003979 finally:
3980 RunGit(['checkout', '-q', branch])
3981 RunGit(['branch', '-D', TMP_BRANCH])
3982
3983 return 0
3984
3985
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003986def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003987 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003988 parser.add_option(
3989 '--no-color',
3990 action='store_true',
3991 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003992 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003993 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003994 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003995
3996 author = RunGit(['config', 'user.email']).strip() or None
3997
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003998 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003999
4000 if args:
4001 if len(args) > 1:
4002 parser.error('Unknown args')
4003 base_branch = args[0]
4004 else:
4005 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004006 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004007
4008 change = cl.GetChange(base_branch, None)
4009 return owners_finder.OwnersFinder(
4010 [f.LocalPath() for f in
4011 cl.GetChange(base_branch, None).AffectedFiles()],
4012 change.RepositoryRoot(), author,
4013 fopen=file, os_path=os.path, glob=glob.glob,
4014 disable_color=options.no_color).run()
4015
4016
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004017def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004018 """Generates a diff command."""
4019 # Generate diff for the current branch's changes.
4020 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4021 upstream_commit, '--' ]
4022
4023 if args:
4024 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004025 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004026 diff_cmd.append(arg)
4027 else:
4028 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004029
4030 return diff_cmd
4031
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004032def MatchingFileType(file_name, extensions):
4033 """Returns true if the file name ends with one of the given extensions."""
4034 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004035
enne@chromium.org555cfe42014-01-29 18:21:39 +00004036@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004037def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004038 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004039 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004040 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004041 parser.add_option('--full', action='store_true',
4042 help='Reformat the full content of all touched files')
4043 parser.add_option('--dry-run', action='store_true',
4044 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004045 parser.add_option('--python', action='store_true',
4046 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004047 parser.add_option('--diff', action='store_true',
4048 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004049 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004050
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004051 # git diff generates paths against the root of the repository. Change
4052 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004053 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004054 if rel_base_path:
4055 os.chdir(rel_base_path)
4056
digit@chromium.org29e47272013-05-17 17:01:46 +00004057 # Grab the merge-base commit, i.e. the upstream commit of the current
4058 # branch when it was created or the last time it was rebased. This is
4059 # to cover the case where the user may have called "git fetch origin",
4060 # moving the origin branch to a newer commit, but hasn't rebased yet.
4061 upstream_commit = None
4062 cl = Changelist()
4063 upstream_branch = cl.GetUpstreamBranch()
4064 if upstream_branch:
4065 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4066 upstream_commit = upstream_commit.strip()
4067
4068 if not upstream_commit:
4069 DieWithError('Could not find base commit for this branch. '
4070 'Are you in detached state?')
4071
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004072 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4073 diff_output = RunGit(changed_files_cmd)
4074 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004075 # Filter out files deleted by this CL
4076 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004077
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004078 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4079 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4080 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004081 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004082
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004083 top_dir = os.path.normpath(
4084 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4085
4086 # Locate the clang-format binary in the checkout
4087 try:
4088 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4089 except clang_format.NotFoundError, e:
4090 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00004091
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004092 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4093 # formatted. This is used to block during the presubmit.
4094 return_value = 0
4095
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004096 if clang_diff_files:
4097 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004098 cmd = [clang_format_tool]
4099 if not opts.dry_run and not opts.diff:
4100 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004101 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004102 if opts.diff:
4103 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004104 else:
4105 env = os.environ.copy()
4106 env['PATH'] = str(os.path.dirname(clang_format_tool))
4107 try:
4108 script = clang_format.FindClangFormatScriptInChromiumTree(
4109 'clang-format-diff.py')
4110 except clang_format.NotFoundError, e:
4111 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004112
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004113 cmd = [sys.executable, script, '-p0']
4114 if not opts.dry_run and not opts.diff:
4115 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004116
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004117 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4118 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004119
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004120 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4121 if opts.diff:
4122 sys.stdout.write(stdout)
4123 if opts.dry_run and len(stdout) > 0:
4124 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004125
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004126 # Similar code to above, but using yapf on .py files rather than clang-format
4127 # on C/C++ files
4128 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004129 yapf_tool = gclient_utils.FindExecutable('yapf')
4130 if yapf_tool is None:
4131 DieWithError('yapf not found in PATH')
4132
4133 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004134 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004135 cmd = [yapf_tool]
4136 if not opts.dry_run and not opts.diff:
4137 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004138 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004139 if opts.diff:
4140 sys.stdout.write(stdout)
4141 else:
4142 # TODO(sbc): yapf --lines mode still has some issues.
4143 # https://github.com/google/yapf/issues/154
4144 DieWithError('--python currently only works with --full')
4145
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004146 # Dart's formatter does not have the nice property of only operating on
4147 # modified chunks, so hard code full.
4148 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004149 try:
4150 command = [dart_format.FindDartFmtToolInChromiumTree()]
4151 if not opts.dry_run and not opts.diff:
4152 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004153 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004154
ppi@chromium.org6593d932016-03-03 15:41:15 +00004155 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004156 if opts.dry_run and stdout:
4157 return_value = 2
4158 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004159 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4160 'found in this checkout. Files in other languages are still ' +
4161 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004162
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004163 # Format GN build files. Always run on full build files for canonical form.
4164 if gn_diff_files:
4165 cmd = ['gn', 'format']
4166 if not opts.dry_run and not opts.diff:
4167 cmd.append('--in-place')
4168 for gn_diff_file in gn_diff_files:
4169 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4170 if opts.diff:
4171 sys.stdout.write(stdout)
4172
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004173 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004174
4175
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004176@subcommand.usage('<codereview url or issue id>')
4177def CMDcheckout(parser, args):
4178 """Checks out a branch associated with a given Rietveld issue."""
4179 _, args = parser.parse_args(args)
4180
4181 if len(args) != 1:
4182 parser.print_help()
4183 return 1
4184
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004185 target_issue = ParseIssueNum(args[0])
4186 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004187 parser.print_help()
4188 return 1
4189
4190 key_and_issues = [x.split() for x in RunGit(
4191 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
4192 .splitlines()]
4193 branches = []
4194 for key, issue in key_and_issues:
4195 if issue == target_issue:
4196 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
4197
4198 if len(branches) == 0:
4199 print 'No branch found for issue %s.' % target_issue
4200 return 1
4201 if len(branches) == 1:
4202 RunGit(['checkout', branches[0]])
4203 else:
4204 print 'Multiple branches match issue %s:' % target_issue
4205 for i in range(len(branches)):
4206 print '%d: %s' % (i, branches[i])
4207 which = raw_input('Choose by index: ')
4208 try:
4209 RunGit(['checkout', branches[int(which)]])
4210 except (IndexError, ValueError):
4211 print 'Invalid selection, not checking out any branch.'
4212 return 1
4213
4214 return 0
4215
4216
maruel@chromium.org29404b52014-09-08 22:58:00 +00004217def CMDlol(parser, args):
4218 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004219 print zlib.decompress(base64.b64decode(
4220 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4221 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4222 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4223 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004224 return 0
4225
4226
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004227class OptionParser(optparse.OptionParser):
4228 """Creates the option parse and add --verbose support."""
4229 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004230 optparse.OptionParser.__init__(
4231 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004232 self.add_option(
4233 '-v', '--verbose', action='count', default=0,
4234 help='Use 2 times for more debugging info')
4235
4236 def parse_args(self, args=None, values=None):
4237 options, args = optparse.OptionParser.parse_args(self, args, values)
4238 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4239 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4240 return options, args
4241
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004242
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004243def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004244 if sys.hexversion < 0x02060000:
4245 print >> sys.stderr, (
4246 '\nYour python version %s is unsupported, please upgrade.\n' %
4247 sys.version.split(' ', 1)[0])
4248 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004249
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004250 # Reload settings.
4251 global settings
4252 settings = Settings()
4253
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004254 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004255 dispatcher = subcommand.CommandDispatcher(__name__)
4256 try:
4257 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004258 except auth.AuthenticationError as e:
4259 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004260 except urllib2.HTTPError, e:
4261 if e.code != 500:
4262 raise
4263 DieWithError(
4264 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4265 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004266 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004267
4268
4269if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004270 # These affect sys.stdout so do it outside of main() to simplify mocks in
4271 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004272 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004273 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004274 try:
4275 sys.exit(main(sys.argv[1:]))
4276 except KeyboardInterrupt:
4277 sys.stderr.write('interrupted\n')
4278 sys.exit(1)