blob: b01b7dbbc22388231d829f68ff72ce2d42ee81dd [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000018import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import optparse
20import os
21import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000025import time
26import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000043from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
maruel@chromium.org0633fb42013-08-16 20:06:14 +000063__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000065DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000066POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000068GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000069REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
thestig@chromium.org44202a22014-03-11 19:22:18 +000074# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000078# Shortcut since it quickly becomes redundant.
79Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000080
maruel@chromium.orgddd59412011-11-30 14:20:38 +000081# Initialized in main()
82settings = None
83
84
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000086 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 sys.exit(1)
88
89
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000090def GetNoGitPagerEnv():
91 env = os.environ.copy()
92 # 'cat' is a magical git string that disables pagers on all platforms.
93 env['GIT_PAGER'] = 'cat'
94 return env
95
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000096
bsep@chromium.org627d9002016-04-29 00:00:52 +000097def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000098 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +000099 return subprocess2.check_output(args, shell=shell, **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):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000133 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142
143
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
maruel@chromium.org90541732011-04-01 17:54:18 +0000151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000160 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000161 if not branch:
162 return
163
164 cmd = ['config']
165 if isinstance(value, int):
166 cmd.append('--int')
167 git_key = 'branch.%s.%s' % (branch, key)
168 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169
170
171def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000172 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000173 if branch:
174 git_key = 'branch.%s.%s' % (branch, key)
175 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
176 try:
177 return int(stdout.strip())
178 except ValueError:
179 pass
180 return default
181
182
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183def add_git_similarity(parser):
184 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000186 help='Sets the percentage that a pair of files need to match in order to'
187 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000188 parser.add_option(
189 '--find-copies', action='store_true',
190 help='Allows git to look for copies.')
191 parser.add_option(
192 '--no-find-copies', action='store_false', dest='find_copies',
193 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000194
195 old_parser_args = parser.parse_args
196 def Parse(args):
197 options, args = old_parser_args(args)
198
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000201 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 print('Note: Saving similarity of %d%% in git config.'
203 % options.similarity)
204 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000205
iannucci@chromium.org79540052012-10-19 23:15:26 +0000206 options.similarity = max(0, min(options.similarity, 100))
207
208 if options.find_copies is None:
209 options.find_copies = bool(
210 git_get_branch_default('git-find-copies', True))
211 else:
212 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000213
214 print('Using %d%% similarity for rename/copy detection. '
215 'Override with --similarity.' % options.similarity)
216
217 return options, args
218 parser.parse_args = Parse
219
220
machenbach@chromium.org45453142015-09-15 08:45:22 +0000221def _get_properties_from_options(options):
222 properties = dict(x.split('=', 1) for x in options.properties)
223 for key, val in properties.iteritems():
224 try:
225 properties[key] = json.loads(val)
226 except ValueError:
227 pass # If a value couldn't be evaluated, treat it as a string.
228 return properties
229
230
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000231def _prefix_master(master):
232 """Convert user-specified master name to full master name.
233
234 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
235 name, while the developers always use shortened master name
236 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
237 function does the conversion for buildbucket migration.
238 """
239 prefix = 'master.'
240 if master.startswith(prefix):
241 return master
242 return '%s%s' % (prefix, master)
243
244
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000245def _buildbucket_retry(operation_name, http, *args, **kwargs):
246 """Retries requests to buildbucket service and returns parsed json content."""
247 try_count = 0
248 while True:
249 response, content = http.request(*args, **kwargs)
250 try:
251 content_json = json.loads(content)
252 except ValueError:
253 content_json = None
254
255 # Buildbucket could return an error even if status==200.
256 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000257 error = content_json.get('error')
258 if error.get('code') == 403:
259 raise BuildbucketResponseException(
260 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000261 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000262 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000263 raise BuildbucketResponseException(msg)
264
265 if response.status == 200:
266 if not content_json:
267 raise BuildbucketResponseException(
268 'Buildbucket returns invalid json content: %s.\n'
269 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
270 content)
271 return content_json
272 if response.status < 500 or try_count >= 2:
273 raise httplib2.HttpLib2Error(content)
274
275 # status >= 500 means transient failures.
276 logging.debug('Transient errors when %s. Will retry.', operation_name)
277 time.sleep(0.5 + 1.5*try_count)
278 try_count += 1
279 assert False, 'unreachable'
280
281
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000282def trigger_luci_job(changelist, masters, options):
283 """Send a job to run on LUCI."""
284 issue_props = changelist.GetIssueProperties()
285 issue = changelist.GetIssue()
286 patchset = changelist.GetMostRecentPatchset()
287 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000288 # TODO(hinoka et al): add support for other properties.
289 # Currently, this completely ignores testfilter and other properties.
290 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000291 luci_trigger.trigger(
292 builder, 'HEAD', issue, patchset, issue_props['project'])
293
294
machenbach@chromium.org45453142015-09-15 08:45:22 +0000295def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000296 rietveld_url = settings.GetDefaultServerUrl()
297 rietveld_host = urlparse.urlparse(rietveld_url).hostname
298 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
299 http = authenticator.authorize(httplib2.Http())
300 http.force_exception_to_status_code = True
301 issue_props = changelist.GetIssueProperties()
302 issue = changelist.GetIssue()
303 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000304 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305
306 buildbucket_put_url = (
307 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000308 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000309 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
310 hostname=rietveld_host,
311 issue=issue,
312 patch=patchset)
313
314 batch_req_body = {'builds': []}
315 print_text = []
316 print_text.append('Tried jobs on:')
317 for master, builders_and_tests in sorted(masters.iteritems()):
318 print_text.append('Master: %s' % master)
319 bucket = _prefix_master(master)
320 for builder, tests in sorted(builders_and_tests.iteritems()):
321 print_text.append(' %s: %s' % (builder, tests))
322 parameters = {
323 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000324 'changes': [{
325 'author': {'email': issue_props['owner_email']},
326 'revision': options.revision,
327 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000328 'properties': {
329 'category': category,
330 'issue': issue,
331 'master': master,
332 'patch_project': issue_props['project'],
333 'patch_storage': 'rietveld',
334 'patchset': patchset,
335 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000336 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 },
338 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000339 if 'presubmit' in builder.lower():
340 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000341 if tests:
342 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000343 if properties:
344 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000345 if options.clobber:
346 parameters['properties']['clobber'] = True
347 batch_req_body['builds'].append(
348 {
349 'bucket': bucket,
350 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000352 'tags': ['builder:%s' % builder,
353 'buildset:%s' % buildset,
354 'master:%s' % master,
355 'user_agent:git_cl_try']
356 }
357 )
358
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000359 _buildbucket_retry(
360 'triggering tryjobs',
361 http,
362 buildbucket_put_url,
363 'PUT',
364 body=json.dumps(batch_req_body),
365 headers={'Content-Type': 'application/json'}
366 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000367 print_text.append('To see results here, run: git cl try-results')
368 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000370
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000372def fetch_try_jobs(auth_config, changelist, options):
373 """Fetches tryjobs from buildbucket.
374
375 Returns a map from build id to build info as json dictionary.
376 """
377 rietveld_url = settings.GetDefaultServerUrl()
378 rietveld_host = urlparse.urlparse(rietveld_url).hostname
379 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
380 if authenticator.has_cached_credentials():
381 http = authenticator.authorize(httplib2.Http())
382 else:
383 print ('Warning: Some results might be missing because %s' %
384 # Get the message on how to login.
385 auth.LoginRequiredError(rietveld_host).message)
386 http = httplib2.Http()
387
388 http.force_exception_to_status_code = True
389
390 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
391 hostname=rietveld_host,
392 issue=changelist.GetIssue(),
393 patch=options.patchset)
394 params = {'tag': 'buildset:%s' % buildset}
395
396 builds = {}
397 while True:
398 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
399 hostname=options.buildbucket_host,
400 params=urllib.urlencode(params))
401 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
402 for build in content.get('builds', []):
403 builds[build['id']] = build
404 if 'next_cursor' in content:
405 params['start_cursor'] = content['next_cursor']
406 else:
407 break
408 return builds
409
410
411def print_tryjobs(options, builds):
412 """Prints nicely result of fetch_try_jobs."""
413 if not builds:
414 print 'No tryjobs scheduled'
415 return
416
417 # Make a copy, because we'll be modifying builds dictionary.
418 builds = builds.copy()
419 builder_names_cache = {}
420
421 def get_builder(b):
422 try:
423 return builder_names_cache[b['id']]
424 except KeyError:
425 try:
426 parameters = json.loads(b['parameters_json'])
427 name = parameters['builder_name']
428 except (ValueError, KeyError) as error:
429 print 'WARNING: failed to get builder name for build %s: %s' % (
430 b['id'], error)
431 name = None
432 builder_names_cache[b['id']] = name
433 return name
434
435 def get_bucket(b):
436 bucket = b['bucket']
437 if bucket.startswith('master.'):
438 return bucket[len('master.'):]
439 return bucket
440
441 if options.print_master:
442 name_fmt = '%%-%ds %%-%ds' % (
443 max(len(str(get_bucket(b))) for b in builds.itervalues()),
444 max(len(str(get_builder(b))) for b in builds.itervalues()))
445 def get_name(b):
446 return name_fmt % (get_bucket(b), get_builder(b))
447 else:
448 name_fmt = '%%-%ds' % (
449 max(len(str(get_builder(b))) for b in builds.itervalues()))
450 def get_name(b):
451 return name_fmt % get_builder(b)
452
453 def sort_key(b):
454 return b['status'], b.get('result'), get_name(b), b.get('url')
455
456 def pop(title, f, color=None, **kwargs):
457 """Pop matching builds from `builds` dict and print them."""
458
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000459 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000460 colorize = str
461 else:
462 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
463
464 result = []
465 for b in builds.values():
466 if all(b.get(k) == v for k, v in kwargs.iteritems()):
467 builds.pop(b['id'])
468 result.append(b)
469 if result:
470 print colorize(title)
471 for b in sorted(result, key=sort_key):
472 print ' ', colorize('\t'.join(map(str, f(b))))
473
474 total = len(builds)
475 pop(status='COMPLETED', result='SUCCESS',
476 title='Successes:', color=Fore.GREEN,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
479 title='Infra Failures:', color=Fore.MAGENTA,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
482 title='Failures:', color=Fore.RED,
483 f=lambda b: (get_name(b), b.get('url')))
484 pop(status='COMPLETED', result='CANCELED',
485 title='Canceled:', color=Fore.MAGENTA,
486 f=lambda b: (get_name(b),))
487 pop(status='COMPLETED', result='FAILURE',
488 failure_reason='INVALID_BUILD_DEFINITION',
489 title='Wrong master/builder name:', color=Fore.MAGENTA,
490 f=lambda b: (get_name(b),))
491 pop(status='COMPLETED', result='FAILURE',
492 title='Other failures:',
493 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
494 pop(status='COMPLETED',
495 title='Other finished:',
496 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
497 pop(status='STARTED',
498 title='Started:', color=Fore.YELLOW,
499 f=lambda b: (get_name(b), b.get('url')))
500 pop(status='SCHEDULED',
501 title='Scheduled:',
502 f=lambda b: (get_name(b), 'id=%s' % b['id']))
503 # The last section is just in case buildbucket API changes OR there is a bug.
504 pop(title='Other:',
505 f=lambda b: (get_name(b), 'id=%s' % b['id']))
506 assert len(builds) == 0
507 print 'Total: %d tryjobs' % total
508
509
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000510def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
511 """Return the corresponding git ref if |base_url| together with |glob_spec|
512 matches the full |url|.
513
514 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
515 """
516 fetch_suburl, as_ref = glob_spec.split(':')
517 if allow_wildcards:
518 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
519 if glob_match:
520 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
521 # "branches/{472,597,648}/src:refs/remotes/svn/*".
522 branch_re = re.escape(base_url)
523 if glob_match.group(1):
524 branch_re += '/' + re.escape(glob_match.group(1))
525 wildcard = glob_match.group(2)
526 if wildcard == '*':
527 branch_re += '([^/]*)'
528 else:
529 # Escape and replace surrounding braces with parentheses and commas
530 # with pipe symbols.
531 wildcard = re.escape(wildcard)
532 wildcard = re.sub('^\\\\{', '(', wildcard)
533 wildcard = re.sub('\\\\,', '|', wildcard)
534 wildcard = re.sub('\\\\}$', ')', wildcard)
535 branch_re += wildcard
536 if glob_match.group(3):
537 branch_re += re.escape(glob_match.group(3))
538 match = re.match(branch_re, url)
539 if match:
540 return re.sub('\*$', match.group(1), as_ref)
541
542 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
543 if fetch_suburl:
544 full_url = base_url + '/' + fetch_suburl
545 else:
546 full_url = base_url
547 if full_url == url:
548 return as_ref
549 return None
550
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000551
iannucci@chromium.org79540052012-10-19 23:15:26 +0000552def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000553 """Prints statistics about the change to the user."""
554 # --no-ext-diff is broken in some versions of Git, so try to work around
555 # this by overriding the environment (but there is still a problem if the
556 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000557 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000558 if 'GIT_EXTERNAL_DIFF' in env:
559 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000560
561 if find_copies:
562 similarity_options = ['--find-copies-harder', '-l100000',
563 '-C%s' % similarity]
564 else:
565 similarity_options = ['-M%s' % similarity]
566
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000567 try:
568 stdout = sys.stdout.fileno()
569 except AttributeError:
570 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000571 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000572 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000573 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000574 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000575
576
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000577class BuildbucketResponseException(Exception):
578 pass
579
580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000581class Settings(object):
582 def __init__(self):
583 self.default_server = None
584 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000585 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586 self.is_git_svn = None
587 self.svn_branch = None
588 self.tree_status_url = None
589 self.viewvc_url = None
590 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000591 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000592 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000593 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000594 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000595 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000596 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000597 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598
599 def LazyUpdateIfNeeded(self):
600 """Updates the settings from a codereview.settings file, if available."""
601 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 # The only value that actually changes the behavior is
603 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000604 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000605 error_ok=True
606 ).strip().lower()
607
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000609 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 LoadCodereviewSettingsFromFile(cr_settings_file)
611 self.updated = True
612
613 def GetDefaultServerUrl(self, error_ok=False):
614 if not self.default_server:
615 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000616 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000617 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618 if error_ok:
619 return self.default_server
620 if not self.default_server:
621 error_message = ('Could not find settings file. You must configure '
622 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000623 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000624 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 return self.default_server
626
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000627 @staticmethod
628 def GetRelativeRoot():
629 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000630
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000632 if self.root is None:
633 self.root = os.path.abspath(self.GetRelativeRoot())
634 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000636 def GetGitMirror(self, remote='origin'):
637 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000638 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000639 if not os.path.isdir(local_url):
640 return None
641 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
642 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
643 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
644 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
645 if mirror.exists():
646 return mirror
647 return None
648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 def GetIsGitSvn(self):
650 """Return true if this repo looks like it's using git-svn."""
651 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000652 if self.GetPendingRefPrefix():
653 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
654 self.is_git_svn = False
655 else:
656 # If you have any "svn-remote.*" config keys, we think you're using svn.
657 self.is_git_svn = RunGitWithCode(
658 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 return self.is_git_svn
660
661 def GetSVNBranch(self):
662 if self.svn_branch is None:
663 if not self.GetIsGitSvn():
664 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
665
666 # Try to figure out which remote branch we're based on.
667 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000668 # 1) iterate through our branch history and find the svn URL.
669 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670
671 # regexp matching the git-svn line that contains the URL.
672 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
673
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000674 # We don't want to go through all of history, so read a line from the
675 # pipe at a time.
676 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000677 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000678 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
679 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000680 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000681 for line in proc.stdout:
682 match = git_svn_re.match(line)
683 if match:
684 url = match.group(1)
685 proc.stdout.close() # Cut pipe.
686 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000688 if url:
689 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
690 remotes = RunGit(['config', '--get-regexp',
691 r'^svn-remote\..*\.url']).splitlines()
692 for remote in remotes:
693 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000694 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000695 remote = match.group(1)
696 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000697 rewrite_root = RunGit(
698 ['config', 'svn-remote.%s.rewriteRoot' % remote],
699 error_ok=True).strip()
700 if rewrite_root:
701 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000702 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000703 ['config', 'svn-remote.%s.fetch' % remote],
704 error_ok=True).strip()
705 if fetch_spec:
706 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
707 if self.svn_branch:
708 break
709 branch_spec = RunGit(
710 ['config', 'svn-remote.%s.branches' % remote],
711 error_ok=True).strip()
712 if branch_spec:
713 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
714 if self.svn_branch:
715 break
716 tag_spec = RunGit(
717 ['config', 'svn-remote.%s.tags' % remote],
718 error_ok=True).strip()
719 if tag_spec:
720 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
721 if self.svn_branch:
722 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723
724 if not self.svn_branch:
725 DieWithError('Can\'t guess svn branch -- try specifying it on the '
726 'command line')
727
728 return self.svn_branch
729
730 def GetTreeStatusUrl(self, error_ok=False):
731 if not self.tree_status_url:
732 error_message = ('You must configure your tree status URL by running '
733 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000734 self.tree_status_url = self._GetRietveldConfig(
735 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 return self.tree_status_url
737
738 def GetViewVCUrl(self):
739 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000740 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 return self.viewvc_url
742
rmistry@google.com90752582014-01-14 21:04:50 +0000743 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000744 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000745
rmistry@google.com78948ed2015-07-08 23:09:57 +0000746 def GetIsSkipDependencyUpload(self, branch_name):
747 """Returns true if specified branch should skip dep uploads."""
748 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
749 error_ok=True)
750
rmistry@google.com5626a922015-02-26 14:03:30 +0000751 def GetRunPostUploadHook(self):
752 run_post_upload_hook = self._GetRietveldConfig(
753 'run-post-upload-hook', error_ok=True)
754 return run_post_upload_hook == "True"
755
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000758
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000761
ukai@chromium.orge8077812012-02-03 03:41:46 +0000762 def GetIsGerrit(self):
763 """Return true if this repo is assosiated with gerrit code review system."""
764 if self.is_gerrit is None:
765 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
766 return self.is_gerrit
767
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000768 def GetSquashGerritUploads(self):
769 """Return true if uploads to Gerrit should be squashed by default."""
770 if self.squash_gerrit_uploads is None:
771 self.squash_gerrit_uploads = (
772 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
773 error_ok=True).strip() == 'true')
774 return self.squash_gerrit_uploads
775
tandrii@chromium.org28253532016-04-14 13:46:56 +0000776 def GetGerritSkipEnsureAuthenticated(self):
777 """Return True if EnsureAuthenticated should not be done for Gerrit
778 uploads."""
779 if self.gerrit_skip_ensure_authenticated is None:
780 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000781 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000782 error_ok=True).strip() == 'true')
783 return self.gerrit_skip_ensure_authenticated
784
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000785 def GetGitEditor(self):
786 """Return the editor specified in the git config, or None if none is."""
787 if self.git_editor is None:
788 self.git_editor = self._GetConfig('core.editor', error_ok=True)
789 return self.git_editor or None
790
thestig@chromium.org44202a22014-03-11 19:22:18 +0000791 def GetLintRegex(self):
792 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
793 DEFAULT_LINT_REGEX)
794
795 def GetLintIgnoreRegex(self):
796 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
797 DEFAULT_LINT_IGNORE_REGEX)
798
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000799 def GetProject(self):
800 if not self.project:
801 self.project = self._GetRietveldConfig('project', error_ok=True)
802 return self.project
803
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000804 def GetForceHttpsCommitUrl(self):
805 if not self.force_https_commit_url:
806 self.force_https_commit_url = self._GetRietveldConfig(
807 'force-https-commit-url', error_ok=True)
808 return self.force_https_commit_url
809
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000810 def GetPendingRefPrefix(self):
811 if not self.pending_ref_prefix:
812 self.pending_ref_prefix = self._GetRietveldConfig(
813 'pending-ref-prefix', error_ok=True)
814 return self.pending_ref_prefix
815
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000816 def _GetRietveldConfig(self, param, **kwargs):
817 return self._GetConfig('rietveld.' + param, **kwargs)
818
rmistry@google.com78948ed2015-07-08 23:09:57 +0000819 def _GetBranchConfig(self, branch_name, param, **kwargs):
820 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
821
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 def _GetConfig(self, param, **kwargs):
823 self.LazyUpdateIfNeeded()
824 return RunGit(['config', param], **kwargs).strip()
825
826
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827def ShortBranchName(branch):
828 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000829 return branch.replace('refs/heads/', '', 1)
830
831
832def GetCurrentBranchRef():
833 """Returns branch ref (e.g., refs/heads/master) or None."""
834 return RunGit(['symbolic-ref', 'HEAD'],
835 stderr=subprocess2.VOID, error_ok=True).strip() or None
836
837
838def GetCurrentBranch():
839 """Returns current branch or None.
840
841 For refs/heads/* branches, returns just last part. For others, full ref.
842 """
843 branchref = GetCurrentBranchRef()
844 if branchref:
845 return ShortBranchName(branchref)
846 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
848
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000849class _CQState(object):
850 """Enum for states of CL with respect to Commit Queue."""
851 NONE = 'none'
852 DRY_RUN = 'dry_run'
853 COMMIT = 'commit'
854
855 ALL_STATES = [NONE, DRY_RUN, COMMIT]
856
857
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000858class _ParsedIssueNumberArgument(object):
859 def __init__(self, issue=None, patchset=None, hostname=None):
860 self.issue = issue
861 self.patchset = patchset
862 self.hostname = hostname
863
864 @property
865 def valid(self):
866 return self.issue is not None
867
868
869class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
870 def __init__(self, *args, **kwargs):
871 self.patch_url = kwargs.pop('patch_url', None)
872 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
873
874
875def ParseIssueNumberArgument(arg):
876 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
877 fail_result = _ParsedIssueNumberArgument()
878
879 if arg.isdigit():
880 return _ParsedIssueNumberArgument(issue=int(arg))
881 if not arg.startswith('http'):
882 return fail_result
883 url = gclient_utils.UpgradeToHttps(arg)
884 try:
885 parsed_url = urlparse.urlparse(url)
886 except ValueError:
887 return fail_result
888 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
889 tmp = cls.ParseIssueURL(parsed_url)
890 if tmp is not None:
891 return tmp
892 return fail_result
893
894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000896 """Changelist works with one changelist in local branch.
897
898 Supports two codereview backends: Rietveld or Gerrit, selected at object
899 creation.
900
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000901 Notes:
902 * Not safe for concurrent multi-{thread,process} use.
903 * Caches values from current branch. Therefore, re-use after branch change
904 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000905 """
906
907 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
908 """Create a new ChangeList instance.
909
910 If issue is given, the codereview must be given too.
911
912 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
913 Otherwise, it's decided based on current configuration of the local branch,
914 with default being 'rietveld' for backwards compatibility.
915 See _load_codereview_impl for more details.
916
917 **kwargs will be passed directly to codereview implementation.
918 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000920 global settings
921 if not settings:
922 # Happens when git_cl.py is used as a utility library.
923 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000924
925 if issue:
926 assert codereview, 'codereview must be known, if issue is known'
927
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 self.branchref = branchref
929 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000930 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931 self.branch = ShortBranchName(self.branchref)
932 else:
933 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000935 self.lookedup_issue = False
936 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937 self.has_description = False
938 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000939 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000941 self.cc = None
942 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000943 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000944
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000946 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000947 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000948 assert self._codereview_impl
949 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000950
951 def _load_codereview_impl(self, codereview=None, **kwargs):
952 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000953 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
954 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
955 self._codereview = codereview
956 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000957 return
958
959 # Automatic selection based on issue number set for a current branch.
960 # Rietveld takes precedence over Gerrit.
961 assert not self.issue
962 # Whether we find issue or not, we are doing the lookup.
963 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000964 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000965 setting = cls.IssueSetting(self.GetBranch())
966 issue = RunGit(['config', setting], error_ok=True).strip()
967 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000968 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969 self._codereview_impl = cls(self, **kwargs)
970 self.issue = int(issue)
971 return
972
973 # No issue is set for this branch, so decide based on repo-wide settings.
974 return self._load_codereview_impl(
975 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
976 **kwargs)
977
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000978 def IsGerrit(self):
979 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000980
981 def GetCCList(self):
982 """Return the users cc'd on this CL.
983
984 Return is a string suitable for passing to gcl with the --cc flag.
985 """
986 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000987 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000988 more_cc = ','.join(self.watchers)
989 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
990 return self.cc
991
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000992 def GetCCListWithoutDefault(self):
993 """Return the users cc'd on this CL excluding default ones."""
994 if self.cc is None:
995 self.cc = ','.join(self.watchers)
996 return self.cc
997
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000998 def SetWatchers(self, watchers):
999 """Set the list of email addresses that should be cc'd based on the changed
1000 files in this CL.
1001 """
1002 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003
1004 def GetBranch(self):
1005 """Returns the short branch name, e.g. 'master'."""
1006 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001007 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001008 if not branchref:
1009 return None
1010 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 self.branch = ShortBranchName(self.branchref)
1012 return self.branch
1013
1014 def GetBranchRef(self):
1015 """Returns the full branch name, e.g. 'refs/heads/master'."""
1016 self.GetBranch() # Poke the lazy loader.
1017 return self.branchref
1018
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001019 def ClearBranch(self):
1020 """Clears cached branch data of this object."""
1021 self.branch = self.branchref = None
1022
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001023 @staticmethod
1024 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001025 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 e.g. 'origin', 'refs/heads/master'
1027 """
1028 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1030 error_ok=True).strip()
1031 if upstream_branch:
1032 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1033 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001034 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1035 error_ok=True).strip()
1036 if upstream_branch:
1037 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001039 # Fall back on trying a git-svn upstream branch.
1040 if settings.GetIsGitSvn():
1041 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001043 # Else, try to guess the origin remote.
1044 remote_branches = RunGit(['branch', '-r']).split()
1045 if 'origin/master' in remote_branches:
1046 # Fall back on origin/master if it exits.
1047 remote = 'origin'
1048 upstream_branch = 'refs/heads/master'
1049 elif 'origin/trunk' in remote_branches:
1050 # Fall back on origin/trunk if it exists. Generally a shared
1051 # git-svn clone
1052 remote = 'origin'
1053 upstream_branch = 'refs/heads/trunk'
1054 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001055 DieWithError(
1056 'Unable to determine default branch to diff against.\n'
1057 'Either pass complete "git diff"-style arguments, like\n'
1058 ' git cl upload origin/master\n'
1059 'or verify this branch is set up to track another \n'
1060 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061
1062 return remote, upstream_branch
1063
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001064 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001065 upstream_branch = self.GetUpstreamBranch()
1066 if not BranchExists(upstream_branch):
1067 DieWithError('The upstream for the current branch (%s) does not exist '
1068 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001069 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001070 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001071
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 def GetUpstreamBranch(self):
1073 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001076 upstream_branch = upstream_branch.replace('refs/heads/',
1077 'refs/remotes/%s/' % remote)
1078 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1079 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 self.upstream_branch = upstream_branch
1081 return self.upstream_branch
1082
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001083 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001084 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001085 remote, branch = None, self.GetBranch()
1086 seen_branches = set()
1087 while branch not in seen_branches:
1088 seen_branches.add(branch)
1089 remote, branch = self.FetchUpstreamTuple(branch)
1090 branch = ShortBranchName(branch)
1091 if remote != '.' or branch.startswith('refs/remotes'):
1092 break
1093 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001094 remotes = RunGit(['remote'], error_ok=True).split()
1095 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001096 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001097 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001098 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001099 logging.warning('Could not determine which remote this change is '
1100 'associated with, so defaulting to "%s". This may '
1101 'not be what you want. You may prevent this message '
1102 'by running "git svn info" as documented here: %s',
1103 self._remote,
1104 GIT_INSTRUCTIONS_URL)
1105 else:
1106 logging.warn('Could not determine which remote this change is '
1107 'associated with. You may prevent this message by '
1108 'running "git svn info" as documented here: %s',
1109 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001110 branch = 'HEAD'
1111 if branch.startswith('refs/remotes'):
1112 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001113 elif branch.startswith('refs/branch-heads/'):
1114 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001115 else:
1116 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001117 return self._remote
1118
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001119 def GitSanityChecks(self, upstream_git_obj):
1120 """Checks git repo status and ensures diff is from local commits."""
1121
sbc@chromium.org79706062015-01-14 21:18:12 +00001122 if upstream_git_obj is None:
1123 if self.GetBranch() is None:
1124 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001125 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001126 else:
1127 print >> sys.stderr, (
1128 'ERROR: no upstream branch')
1129 return False
1130
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001131 # Verify the commit we're diffing against is in our current branch.
1132 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1133 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1134 if upstream_sha != common_ancestor:
1135 print >> sys.stderr, (
1136 'ERROR: %s is not in the current branch. You may need to rebase '
1137 'your tracking branch' % upstream_sha)
1138 return False
1139
1140 # List the commits inside the diff, and verify they are all local.
1141 commits_in_diff = RunGit(
1142 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1143 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1144 remote_branch = remote_branch.strip()
1145 if code != 0:
1146 _, remote_branch = self.GetRemoteBranch()
1147
1148 commits_in_remote = RunGit(
1149 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1150
1151 common_commits = set(commits_in_diff) & set(commits_in_remote)
1152 if common_commits:
1153 print >> sys.stderr, (
1154 'ERROR: Your diff contains %d commits already in %s.\n'
1155 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1156 'the diff. If you are using a custom git flow, you can override'
1157 ' the reference used for this check with "git config '
1158 'gitcl.remotebranch <git-ref>".' % (
1159 len(common_commits), remote_branch, upstream_git_obj))
1160 return False
1161 return True
1162
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001163 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001164 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001165
1166 Returns None if it is not set.
1167 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001168 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1169 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001170
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001171 def GetGitSvnRemoteUrl(self):
1172 """Return the configured git-svn remote URL parsed from git svn info.
1173
1174 Returns None if it is not set.
1175 """
1176 # URL is dependent on the current directory.
1177 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1178 if data:
1179 keys = dict(line.split(': ', 1) for line in data.splitlines()
1180 if ': ' in line)
1181 return keys.get('URL', None)
1182 return None
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 def GetRemoteUrl(self):
1185 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1186
1187 Returns None if there is no remote.
1188 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001190 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1191
1192 # If URL is pointing to a local directory, it is probably a git cache.
1193 if os.path.isdir(url):
1194 url = RunGit(['config', 'remote.%s.url' % remote],
1195 error_ok=True,
1196 cwd=url).strip()
1197 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001199 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001200 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001201 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001202 issue = RunGit(['config',
1203 self._codereview_impl.IssueSetting(self.GetBranch())],
1204 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 self.issue = int(issue) or None if issue else None
1206 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 return self.issue
1208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 def GetIssueURL(self):
1210 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001211 issue = self.GetIssue()
1212 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001213 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215
1216 def GetDescription(self, pretty=False):
1217 if not self.has_description:
1218 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 self.has_description = True
1221 if pretty:
1222 wrapper = textwrap.TextWrapper()
1223 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1224 return wrapper.fill(self.description)
1225 return self.description
1226
1227 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001228 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001229 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001230 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001232 self.patchset = int(patchset) or None if patchset else None
1233 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 return self.patchset
1235
1236 def SetPatchset(self, patchset):
1237 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001238 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001240 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001241 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001243 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001244 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001245 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001247 def SetIssue(self, issue=None):
1248 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001249 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1250 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001252 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001253 RunGit(['config', issue_setting, str(issue)])
1254 codereview_server = self._codereview_impl.GetCodereviewServer()
1255 if codereview_server:
1256 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 else:
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001258 # Reset it regardless. It doesn't hurt.
1259 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1260 for prop in (['last-upload-hash'] +
1261 self._codereview_impl._PostUnsetIssueProperties()):
1262 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1263 for setting in config_settings:
1264 RunGit(['config', '--unset', setting], error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001265 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001266 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001268 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001269 if not self.GitSanityChecks(upstream_branch):
1270 DieWithError('\nGit sanity check failure')
1271
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001272 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001273 if not root:
1274 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001275 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001276
1277 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001278 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001279 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001280 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001281 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001282 except subprocess2.CalledProcessError:
1283 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001284 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001285 'This branch probably doesn\'t exist anymore. To reset the\n'
1286 'tracking branch, please run\n'
1287 ' git branch --set-upstream %s trunk\n'
1288 'replacing trunk with origin/master or the relevant branch') %
1289 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001290
maruel@chromium.org52424302012-08-29 15:14:30 +00001291 issue = self.GetIssue()
1292 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001293 if issue:
1294 description = self.GetDescription()
1295 else:
1296 # If the change was never uploaded, use the log messages of all commits
1297 # up to the branch point, as git cl upload will prefill the description
1298 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001299 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1300 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001301
1302 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001303 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001304 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001305 name,
1306 description,
1307 absroot,
1308 files,
1309 issue,
1310 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001311 author,
1312 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001313
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001314 def UpdateDescription(self, description):
1315 self.description = description
1316 return self._codereview_impl.UpdateDescriptionRemote(description)
1317
1318 def RunHook(self, committing, may_prompt, verbose, change):
1319 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1320 try:
1321 return presubmit_support.DoPresubmitChecks(change, committing,
1322 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1323 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001324 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1325 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001326 except presubmit_support.PresubmitFailure, e:
1327 DieWithError(
1328 ('%s\nMaybe your depot_tools is out of date?\n'
1329 'If all fails, contact maruel@') % e)
1330
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001331 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1332 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001333 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1334 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001335 else:
1336 # Assume url.
1337 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1338 urlparse.urlparse(issue_arg))
1339 if not parsed_issue_arg or not parsed_issue_arg.valid:
1340 DieWithError('Failed to parse issue argument "%s". '
1341 'Must be an issue number or a valid URL.' % issue_arg)
1342 return self._codereview_impl.CMDPatchWithParsedIssue(
1343 parsed_issue_arg, reject, nocommit, directory)
1344
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001345 def CMDUpload(self, options, git_diff_args, orig_args):
1346 """Uploads a change to codereview."""
1347 if git_diff_args:
1348 # TODO(ukai): is it ok for gerrit case?
1349 base_branch = git_diff_args[0]
1350 else:
1351 if self.GetBranch() is None:
1352 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1353
1354 # Default to diffing against common ancestor of upstream branch
1355 base_branch = self.GetCommonAncestorWithUpstream()
1356 git_diff_args = [base_branch, 'HEAD']
1357
1358 # Make sure authenticated to codereview before running potentially expensive
1359 # hooks. It is a fast, best efforts check. Codereview still can reject the
1360 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001361 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001362
1363 # Apply watchlists on upload.
1364 change = self.GetChange(base_branch, None)
1365 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1366 files = [f.LocalPath() for f in change.AffectedFiles()]
1367 if not options.bypass_watchlists:
1368 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1369
1370 if not options.bypass_hooks:
1371 if options.reviewers or options.tbr_owners:
1372 # Set the reviewer list now so that presubmit checks can access it.
1373 change_description = ChangeDescription(change.FullDescriptionText())
1374 change_description.update_reviewers(options.reviewers,
1375 options.tbr_owners,
1376 change)
1377 change.SetDescriptionText(change_description.description)
1378 hook_results = self.RunHook(committing=False,
1379 may_prompt=not options.force,
1380 verbose=options.verbose,
1381 change=change)
1382 if not hook_results.should_continue():
1383 return 1
1384 if not options.reviewers and hook_results.reviewers:
1385 options.reviewers = hook_results.reviewers.split(',')
1386
1387 if self.GetIssue():
1388 latest_patchset = self.GetMostRecentPatchset()
1389 local_patchset = self.GetPatchset()
1390 if (latest_patchset and local_patchset and
1391 local_patchset != latest_patchset):
1392 print ('The last upload made from this repository was patchset #%d but '
1393 'the most recent patchset on the server is #%d.'
1394 % (local_patchset, latest_patchset))
1395 print ('Uploading will still work, but if you\'ve uploaded to this '
1396 'issue from another machine or branch the patch you\'re '
1397 'uploading now might not include those changes.')
1398 ask_for_data('About to upload; enter to confirm.')
1399
1400 print_stats(options.similarity, options.find_copies, git_diff_args)
1401 ret = self.CMDUploadChange(options, git_diff_args, change)
1402 if not ret:
1403 git_set_branch_value('last-upload-hash',
1404 RunGit(['rev-parse', 'HEAD']).strip())
1405 # Run post upload hooks, if specified.
1406 if settings.GetRunPostUploadHook():
1407 presubmit_support.DoPostUploadExecuter(
1408 change,
1409 self,
1410 settings.GetRoot(),
1411 options.verbose,
1412 sys.stdout)
1413
1414 # Upload all dependencies if specified.
1415 if options.dependencies:
1416 print
1417 print '--dependencies has been specified.'
1418 print 'All dependent local branches will be re-uploaded.'
1419 print
1420 # Remove the dependencies flag from args so that we do not end up in a
1421 # loop.
1422 orig_args.remove('--dependencies')
1423 ret = upload_branch_deps(self, orig_args)
1424 return ret
1425
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001426 def SetCQState(self, new_state):
1427 """Update the CQ state for latest patchset.
1428
1429 Issue must have been already uploaded and known.
1430 """
1431 assert new_state in _CQState.ALL_STATES
1432 assert self.GetIssue()
1433 return self._codereview_impl.SetCQState(new_state)
1434
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001435 # Forward methods to codereview specific implementation.
1436
1437 def CloseIssue(self):
1438 return self._codereview_impl.CloseIssue()
1439
1440 def GetStatus(self):
1441 return self._codereview_impl.GetStatus()
1442
1443 def GetCodereviewServer(self):
1444 return self._codereview_impl.GetCodereviewServer()
1445
1446 def GetApprovingReviewers(self):
1447 return self._codereview_impl.GetApprovingReviewers()
1448
1449 def GetMostRecentPatchset(self):
1450 return self._codereview_impl.GetMostRecentPatchset()
1451
1452 def __getattr__(self, attr):
1453 # This is because lots of untested code accesses Rietveld-specific stuff
1454 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001455 # on a case by case basis.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001456 return getattr(self._codereview_impl, attr)
1457
1458
1459class _ChangelistCodereviewBase(object):
1460 """Abstract base class encapsulating codereview specifics of a changelist."""
1461 def __init__(self, changelist):
1462 self._changelist = changelist # instance of Changelist
1463
1464 def __getattr__(self, attr):
1465 # Forward methods to changelist.
1466 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1467 # _RietveldChangelistImpl to avoid this hack?
1468 return getattr(self._changelist, attr)
1469
1470 def GetStatus(self):
1471 """Apply a rough heuristic to give a simple summary of an issue's review
1472 or CQ status, assuming adherence to a common workflow.
1473
1474 Returns None if no issue for this branch, or specific string keywords.
1475 """
1476 raise NotImplementedError()
1477
1478 def GetCodereviewServer(self):
1479 """Returns server URL without end slash, like "https://codereview.com"."""
1480 raise NotImplementedError()
1481
1482 def FetchDescription(self):
1483 """Fetches and returns description from the codereview server."""
1484 raise NotImplementedError()
1485
1486 def GetCodereviewServerSetting(self):
1487 """Returns git config setting for the codereview server."""
1488 raise NotImplementedError()
1489
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001490 @classmethod
1491 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001492 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001493
1494 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001495 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 """Returns name of git config setting which stores issue number for a given
1497 branch."""
1498 raise NotImplementedError()
1499
1500 def PatchsetSetting(self):
1501 """Returns name of git config setting which stores issue number."""
1502 raise NotImplementedError()
1503
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001504 def _PostUnsetIssueProperties(self):
1505 """Which branch-specific properties to erase when unsettin issue."""
1506 raise NotImplementedError()
1507
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001508 def GetRieveldObjForPresubmit(self):
1509 # This is an unfortunate Rietveld-embeddedness in presubmit.
1510 # For non-Rietveld codereviews, this probably should return a dummy object.
1511 raise NotImplementedError()
1512
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001513 def GetGerritObjForPresubmit(self):
1514 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1515 return None
1516
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001517 def UpdateDescriptionRemote(self, description):
1518 """Update the description on codereview site."""
1519 raise NotImplementedError()
1520
1521 def CloseIssue(self):
1522 """Closes the issue."""
1523 raise NotImplementedError()
1524
1525 def GetApprovingReviewers(self):
1526 """Returns a list of reviewers approving the change.
1527
1528 Note: not necessarily committers.
1529 """
1530 raise NotImplementedError()
1531
1532 def GetMostRecentPatchset(self):
1533 """Returns the most recent patchset number from the codereview site."""
1534 raise NotImplementedError()
1535
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001536 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1537 directory):
1538 """Fetches and applies the issue.
1539
1540 Arguments:
1541 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1542 reject: if True, reject the failed patch instead of switching to 3-way
1543 merge. Rietveld only.
1544 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1545 only.
1546 directory: switch to directory before applying the patch. Rietveld only.
1547 """
1548 raise NotImplementedError()
1549
1550 @staticmethod
1551 def ParseIssueURL(parsed_url):
1552 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1553 failed."""
1554 raise NotImplementedError()
1555
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001556 def EnsureAuthenticated(self, force):
1557 """Best effort check that user is authenticated with codereview server.
1558
1559 Arguments:
1560 force: whether to skip confirmation questions.
1561 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001562 raise NotImplementedError()
1563
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001564 def CMDUploadChange(self, options, args, change):
1565 """Uploads a change to codereview."""
1566 raise NotImplementedError()
1567
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001568 def SetCQState(self, new_state):
1569 """Update the CQ state for latest patchset.
1570
1571 Issue must have been already uploaded and known.
1572 """
1573 raise NotImplementedError()
1574
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001575
1576class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1577 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1578 super(_RietveldChangelistImpl, self).__init__(changelist)
1579 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1580 settings.GetDefaultServerUrl()
1581
1582 self._rietveld_server = rietveld_server
1583 self._auth_config = auth_config
1584 self._props = None
1585 self._rpc_server = None
1586
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001587 def GetCodereviewServer(self):
1588 if not self._rietveld_server:
1589 # If we're on a branch then get the server potentially associated
1590 # with that branch.
1591 if self.GetIssue():
1592 rietveld_server_setting = self.GetCodereviewServerSetting()
1593 if rietveld_server_setting:
1594 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1595 ['config', rietveld_server_setting], error_ok=True).strip())
1596 if not self._rietveld_server:
1597 self._rietveld_server = settings.GetDefaultServerUrl()
1598 return self._rietveld_server
1599
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001600 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001601 """Best effort check that user is authenticated with Rietveld server."""
1602 if self._auth_config.use_oauth2:
1603 authenticator = auth.get_authenticator_for_host(
1604 self.GetCodereviewServer(), self._auth_config)
1605 if not authenticator.has_cached_credentials():
1606 raise auth.LoginRequiredError(self.GetCodereviewServer())
1607
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001608 def FetchDescription(self):
1609 issue = self.GetIssue()
1610 assert issue
1611 try:
1612 return self.RpcServer().get_description(issue).strip()
1613 except urllib2.HTTPError as e:
1614 if e.code == 404:
1615 DieWithError(
1616 ('\nWhile fetching the description for issue %d, received a '
1617 '404 (not found)\n'
1618 'error. It is likely that you deleted this '
1619 'issue on the server. If this is the\n'
1620 'case, please run\n\n'
1621 ' git cl issue 0\n\n'
1622 'to clear the association with the deleted issue. Then run '
1623 'this command again.') % issue)
1624 else:
1625 DieWithError(
1626 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1627 except urllib2.URLError as e:
1628 print >> sys.stderr, (
1629 'Warning: Failed to retrieve CL description due to network '
1630 'failure.')
1631 return ''
1632
1633 def GetMostRecentPatchset(self):
1634 return self.GetIssueProperties()['patchsets'][-1]
1635
1636 def GetPatchSetDiff(self, issue, patchset):
1637 return self.RpcServer().get(
1638 '/download/issue%s_%s.diff' % (issue, patchset))
1639
1640 def GetIssueProperties(self):
1641 if self._props is None:
1642 issue = self.GetIssue()
1643 if not issue:
1644 self._props = {}
1645 else:
1646 self._props = self.RpcServer().get_issue_properties(issue, True)
1647 return self._props
1648
1649 def GetApprovingReviewers(self):
1650 return get_approving_reviewers(self.GetIssueProperties())
1651
1652 def AddComment(self, message):
1653 return self.RpcServer().add_comment(self.GetIssue(), message)
1654
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001655 def GetStatus(self):
1656 """Apply a rough heuristic to give a simple summary of an issue's review
1657 or CQ status, assuming adherence to a common workflow.
1658
1659 Returns None if no issue for this branch, or one of the following keywords:
1660 * 'error' - error from review tool (including deleted issues)
1661 * 'unsent' - not sent for review
1662 * 'waiting' - waiting for review
1663 * 'reply' - waiting for owner to reply to review
1664 * 'lgtm' - LGTM from at least one approved reviewer
1665 * 'commit' - in the commit queue
1666 * 'closed' - closed
1667 """
1668 if not self.GetIssue():
1669 return None
1670
1671 try:
1672 props = self.GetIssueProperties()
1673 except urllib2.HTTPError:
1674 return 'error'
1675
1676 if props.get('closed'):
1677 # Issue is closed.
1678 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001679 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001680 # Issue is in the commit queue.
1681 return 'commit'
1682
1683 try:
1684 reviewers = self.GetApprovingReviewers()
1685 except urllib2.HTTPError:
1686 return 'error'
1687
1688 if reviewers:
1689 # Was LGTM'ed.
1690 return 'lgtm'
1691
1692 messages = props.get('messages') or []
1693
1694 if not messages:
1695 # No message was sent.
1696 return 'unsent'
1697 if messages[-1]['sender'] != props.get('owner_email'):
1698 # Non-LGTM reply from non-owner
1699 return 'reply'
1700 return 'waiting'
1701
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001702 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001703 return self.RpcServer().update_description(
1704 self.GetIssue(), self.description)
1705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001707 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001709 def SetFlag(self, flag, value):
1710 """Patchset must match."""
1711 if not self.GetPatchset():
1712 DieWithError('The patchset needs to match. Send another patchset.')
1713 try:
1714 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001715 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001716 except urllib2.HTTPError, e:
1717 if e.code == 404:
1718 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1719 if e.code == 403:
1720 DieWithError(
1721 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1722 'match?') % (self.GetIssue(), self.GetPatchset()))
1723 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001725 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 """Returns an upload.RpcServer() to access this review's rietveld instance.
1727 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001728 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001729 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001730 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001731 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001732 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001733
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001734 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001735 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001736 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001738 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739 """Return the git setting that stores this change's most recent patchset."""
1740 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1741
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001742 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001743 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001744 branch = self.GetBranch()
1745 if branch:
1746 return 'branch.%s.rietveldserver' % branch
1747 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001748
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001749 def _PostUnsetIssueProperties(self):
1750 """Which branch-specific properties to erase when unsetting issue."""
1751 return ['rietveldserver']
1752
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 def GetRieveldObjForPresubmit(self):
1754 return self.RpcServer()
1755
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001756 def SetCQState(self, new_state):
1757 props = self.GetIssueProperties()
1758 if props.get('private'):
1759 DieWithError('Cannot set-commit on private issue')
1760
1761 if new_state == _CQState.COMMIT:
1762 self.SetFlag('commit', '1')
1763 elif new_state == _CQState.NONE:
1764 self.SetFlag('commit', '0')
1765 else:
1766 raise NotImplementedError()
1767
1768
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001769 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1770 directory):
1771 # TODO(maruel): Use apply_issue.py
1772
1773 # PatchIssue should never be called with a dirty tree. It is up to the
1774 # caller to check this, but just in case we assert here since the
1775 # consequences of the caller not checking this could be dire.
1776 assert(not git_common.is_dirty_git_tree('apply'))
1777 assert(parsed_issue_arg.valid)
1778 self._changelist.issue = parsed_issue_arg.issue
1779 if parsed_issue_arg.hostname:
1780 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1781
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001782 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1783 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001784 assert parsed_issue_arg.patchset
1785 patchset = parsed_issue_arg.patchset
1786 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1787 else:
1788 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1789 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1790
1791 # Switch up to the top-level directory, if necessary, in preparation for
1792 # applying the patch.
1793 top = settings.GetRelativeRoot()
1794 if top:
1795 os.chdir(top)
1796
1797 # Git patches have a/ at the beginning of source paths. We strip that out
1798 # with a sed script rather than the -p flag to patch so we can feed either
1799 # Git or svn-style patches into the same apply command.
1800 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1801 try:
1802 patch_data = subprocess2.check_output(
1803 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1804 except subprocess2.CalledProcessError:
1805 DieWithError('Git patch mungling failed.')
1806 logging.info(patch_data)
1807
1808 # We use "git apply" to apply the patch instead of "patch" so that we can
1809 # pick up file adds.
1810 # The --index flag means: also insert into the index (so we catch adds).
1811 cmd = ['git', 'apply', '--index', '-p0']
1812 if directory:
1813 cmd.extend(('--directory', directory))
1814 if reject:
1815 cmd.append('--reject')
1816 elif IsGitVersionAtLeast('1.7.12'):
1817 cmd.append('--3way')
1818 try:
1819 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1820 stdin=patch_data, stdout=subprocess2.VOID)
1821 except subprocess2.CalledProcessError:
1822 print 'Failed to apply the patch'
1823 return 1
1824
1825 # If we had an issue, commit the current state and register the issue.
1826 if not nocommit:
1827 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1828 'patch from issue %(i)s at patchset '
1829 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1830 % {'i': self.GetIssue(), 'p': patchset})])
1831 self.SetIssue(self.GetIssue())
1832 self.SetPatchset(patchset)
1833 print "Committed patch locally."
1834 else:
1835 print "Patch applied to index."
1836 return 0
1837
1838 @staticmethod
1839 def ParseIssueURL(parsed_url):
1840 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1841 return None
1842 # Typical url: https://domain/<issue_number>[/[other]]
1843 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1844 if match:
1845 return _RietveldParsedIssueNumberArgument(
1846 issue=int(match.group(1)),
1847 hostname=parsed_url.netloc)
1848 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1849 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1850 if match:
1851 return _RietveldParsedIssueNumberArgument(
1852 issue=int(match.group(1)),
1853 patchset=int(match.group(2)),
1854 hostname=parsed_url.netloc,
1855 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1856 return None
1857
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001858 def CMDUploadChange(self, options, args, change):
1859 """Upload the patch to Rietveld."""
1860 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1861 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001862 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1863 if options.emulate_svn_auto_props:
1864 upload_args.append('--emulate_svn_auto_props')
1865
1866 change_desc = None
1867
1868 if options.email is not None:
1869 upload_args.extend(['--email', options.email])
1870
1871 if self.GetIssue():
1872 if options.title:
1873 upload_args.extend(['--title', options.title])
1874 if options.message:
1875 upload_args.extend(['--message', options.message])
1876 upload_args.extend(['--issue', str(self.GetIssue())])
1877 print ('This branch is associated with issue %s. '
1878 'Adding patch to that issue.' % self.GetIssue())
1879 else:
1880 if options.title:
1881 upload_args.extend(['--title', options.title])
1882 message = (options.title or options.message or
1883 CreateDescriptionFromLog(args))
1884 change_desc = ChangeDescription(message)
1885 if options.reviewers or options.tbr_owners:
1886 change_desc.update_reviewers(options.reviewers,
1887 options.tbr_owners,
1888 change)
1889 if not options.force:
1890 change_desc.prompt()
1891
1892 if not change_desc.description:
1893 print "Description is empty; aborting."
1894 return 1
1895
1896 upload_args.extend(['--message', change_desc.description])
1897 if change_desc.get_reviewers():
1898 upload_args.append('--reviewers=%s' % ','.join(
1899 change_desc.get_reviewers()))
1900 if options.send_mail:
1901 if not change_desc.get_reviewers():
1902 DieWithError("Must specify reviewers to send email.")
1903 upload_args.append('--send_mail')
1904
1905 # We check this before applying rietveld.private assuming that in
1906 # rietveld.cc only addresses which we can send private CLs to are listed
1907 # if rietveld.private is set, and so we should ignore rietveld.cc only
1908 # when --private is specified explicitly on the command line.
1909 if options.private:
1910 logging.warn('rietveld.cc is ignored since private flag is specified. '
1911 'You need to review and add them manually if necessary.')
1912 cc = self.GetCCListWithoutDefault()
1913 else:
1914 cc = self.GetCCList()
1915 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1916 if cc:
1917 upload_args.extend(['--cc', cc])
1918
1919 if options.private or settings.GetDefaultPrivateFlag() == "True":
1920 upload_args.append('--private')
1921
1922 upload_args.extend(['--git_similarity', str(options.similarity)])
1923 if not options.find_copies:
1924 upload_args.extend(['--git_no_find_copies'])
1925
1926 # Include the upstream repo's URL in the change -- this is useful for
1927 # projects that have their source spread across multiple repos.
1928 remote_url = self.GetGitBaseUrlFromConfig()
1929 if not remote_url:
1930 if settings.GetIsGitSvn():
1931 remote_url = self.GetGitSvnRemoteUrl()
1932 else:
1933 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1934 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1935 self.GetUpstreamBranch().split('/')[-1])
1936 if remote_url:
1937 upload_args.extend(['--base_url', remote_url])
1938 remote, remote_branch = self.GetRemoteBranch()
1939 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1940 settings.GetPendingRefPrefix())
1941 if target_ref:
1942 upload_args.extend(['--target_ref', target_ref])
1943
1944 # Look for dependent patchsets. See crbug.com/480453 for more details.
1945 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1946 upstream_branch = ShortBranchName(upstream_branch)
1947 if remote is '.':
1948 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001949 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001950 if settings.GetIsSkipDependencyUpload(local_branch):
1951 print
1952 print ('Skipping dependency patchset upload because git config '
1953 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1954 print
1955 else:
1956 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001957 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001958 auth_config=auth_config)
1959 branch_cl_issue_url = branch_cl.GetIssueURL()
1960 branch_cl_issue = branch_cl.GetIssue()
1961 branch_cl_patchset = branch_cl.GetPatchset()
1962 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1963 upload_args.extend(
1964 ['--depends_on_patchset', '%s:%s' % (
1965 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001966 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001967 '\n'
1968 'The current branch (%s) is tracking a local branch (%s) with '
1969 'an associated CL.\n'
1970 'Adding %s/#ps%s as a dependency patchset.\n'
1971 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1972 branch_cl_patchset))
1973
1974 project = settings.GetProject()
1975 if project:
1976 upload_args.extend(['--project', project])
1977
1978 if options.cq_dry_run:
1979 upload_args.extend(['--cq_dry_run'])
1980
1981 try:
1982 upload_args = ['upload'] + upload_args + args
1983 logging.info('upload.RealMain(%s)', upload_args)
1984 issue, patchset = upload.RealMain(upload_args)
1985 issue = int(issue)
1986 patchset = int(patchset)
1987 except KeyboardInterrupt:
1988 sys.exit(1)
1989 except:
1990 # If we got an exception after the user typed a description for their
1991 # change, back up the description before re-raising.
1992 if change_desc:
1993 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1994 print('\nGot exception while uploading -- saving description to %s\n' %
1995 backup_path)
1996 backup_file = open(backup_path, 'w')
1997 backup_file.write(change_desc.description)
1998 backup_file.close()
1999 raise
2000
2001 if not self.GetIssue():
2002 self.SetIssue(issue)
2003 self.SetPatchset(patchset)
2004
2005 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002006 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002007 return 0
2008
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002009
2010class _GerritChangelistImpl(_ChangelistCodereviewBase):
2011 def __init__(self, changelist, auth_config=None):
2012 # auth_config is Rietveld thing, kept here to preserve interface only.
2013 super(_GerritChangelistImpl, self).__init__(changelist)
2014 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002015 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002016 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002017 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002018
2019 def _GetGerritHost(self):
2020 # Lazy load of configs.
2021 self.GetCodereviewServer()
2022 return self._gerrit_host
2023
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002024 def _GetGitHost(self):
2025 """Returns git host to be used when uploading change to Gerrit."""
2026 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2027
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002028 def GetCodereviewServer(self):
2029 if not self._gerrit_server:
2030 # If we're on a branch then get the server potentially associated
2031 # with that branch.
2032 if self.GetIssue():
2033 gerrit_server_setting = self.GetCodereviewServerSetting()
2034 if gerrit_server_setting:
2035 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2036 error_ok=True).strip()
2037 if self._gerrit_server:
2038 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2039 if not self._gerrit_server:
2040 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2041 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002042 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002043 parts[0] = parts[0] + '-review'
2044 self._gerrit_host = '.'.join(parts)
2045 self._gerrit_server = 'https://%s' % self._gerrit_host
2046 return self._gerrit_server
2047
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002048 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002049 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002050 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002051
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002052 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002053 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002054 if settings.GetGerritSkipEnsureAuthenticated():
2055 # For projects with unusual authentication schemes.
2056 # See http://crbug.com/603378.
2057 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002058 # Lazy-loader to identify Gerrit and Git hosts.
2059 if gerrit_util.GceAuthenticator.is_gce():
2060 return
2061 self.GetCodereviewServer()
2062 git_host = self._GetGitHost()
2063 assert self._gerrit_server and self._gerrit_host
2064 cookie_auth = gerrit_util.CookiesAuthenticator()
2065
2066 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2067 git_auth = cookie_auth.get_auth_header(git_host)
2068 if gerrit_auth and git_auth:
2069 if gerrit_auth == git_auth:
2070 return
2071 print((
2072 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2073 ' Check your %s or %s file for credentials of hosts:\n'
2074 ' %s\n'
2075 ' %s\n'
2076 ' %s') %
2077 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2078 git_host, self._gerrit_host,
2079 cookie_auth.get_new_password_message(git_host)))
2080 if not force:
2081 ask_for_data('If you know what you are doing, press Enter to continue, '
2082 'Ctrl+C to abort.')
2083 return
2084 else:
2085 missing = (
2086 [] if gerrit_auth else [self._gerrit_host] +
2087 [] if git_auth else [git_host])
2088 DieWithError('Credentials for the following hosts are required:\n'
2089 ' %s\n'
2090 'These are read from %s (or legacy %s)\n'
2091 '%s' % (
2092 '\n '.join(missing),
2093 cookie_auth.get_gitcookies_path(),
2094 cookie_auth.get_netrc_path(),
2095 cookie_auth.get_new_password_message(git_host)))
2096
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002097
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002098 def PatchsetSetting(self):
2099 """Return the git setting that stores this change's most recent patchset."""
2100 return 'branch.%s.gerritpatchset' % self.GetBranch()
2101
2102 def GetCodereviewServerSetting(self):
2103 """Returns the git setting that stores this change's Gerrit server."""
2104 branch = self.GetBranch()
2105 if branch:
2106 return 'branch.%s.gerritserver' % branch
2107 return None
2108
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002109 def _PostUnsetIssueProperties(self):
2110 """Which branch-specific properties to erase when unsetting issue."""
2111 return [
2112 'gerritserver',
2113 'gerritsquashhash',
2114 ]
2115
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002116 def GetRieveldObjForPresubmit(self):
2117 class ThisIsNotRietveldIssue(object):
2118 def __nonzero__(self):
2119 # This is a hack to make presubmit_support think that rietveld is not
2120 # defined, yet still ensure that calls directly result in a decent
2121 # exception message below.
2122 return False
2123
2124 def __getattr__(self, attr):
2125 print(
2126 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2127 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2128 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2129 'or use Rietveld for codereview.\n'
2130 'See also http://crbug.com/579160.' % attr)
2131 raise NotImplementedError()
2132 return ThisIsNotRietveldIssue()
2133
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002134 def GetGerritObjForPresubmit(self):
2135 return presubmit_support.GerritAccessor(self._GetGerritHost())
2136
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002137 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002138 """Apply a rough heuristic to give a simple summary of an issue's review
2139 or CQ status, assuming adherence to a common workflow.
2140
2141 Returns None if no issue for this branch, or one of the following keywords:
2142 * 'error' - error from review tool (including deleted issues)
2143 * 'unsent' - no reviewers added
2144 * 'waiting' - waiting for review
2145 * 'reply' - waiting for owner to reply to review
2146 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2147 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2148 * 'commit' - in the commit queue
2149 * 'closed' - abandoned
2150 """
2151 if not self.GetIssue():
2152 return None
2153
2154 try:
2155 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2156 except httplib.HTTPException:
2157 return 'error'
2158
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002159 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002160 return 'closed'
2161
2162 cq_label = data['labels'].get('Commit-Queue', {})
2163 if cq_label:
2164 # Vote value is a stringified integer, which we expect from 0 to 2.
2165 vote_value = cq_label.get('value', '0')
2166 vote_text = cq_label.get('values', {}).get(vote_value, '')
2167 if vote_text.lower() == 'commit':
2168 return 'commit'
2169
2170 lgtm_label = data['labels'].get('Code-Review', {})
2171 if lgtm_label:
2172 if 'rejected' in lgtm_label:
2173 return 'not lgtm'
2174 if 'approved' in lgtm_label:
2175 return 'lgtm'
2176
2177 if not data.get('reviewers', {}).get('REVIEWER', []):
2178 return 'unsent'
2179
2180 messages = data.get('messages', [])
2181 if messages:
2182 owner = data['owner'].get('_account_id')
2183 last_message_author = messages[-1].get('author', {}).get('_account_id')
2184 if owner != last_message_author:
2185 # Some reply from non-owner.
2186 return 'reply'
2187
2188 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002189
2190 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002191 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002192 return data['revisions'][data['current_revision']]['_number']
2193
2194 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002195 data = self._GetChangeDetail(['CURRENT_REVISION'])
2196 current_rev = data['current_revision']
2197 url = data['revisions'][current_rev]['fetch']['http']['url']
2198 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002199
2200 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002201 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2202 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002203
2204 def CloseIssue(self):
2205 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2206
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002207 def GetApprovingReviewers(self):
2208 """Returns a list of reviewers approving the change.
2209
2210 Note: not necessarily committers.
2211 """
2212 raise NotImplementedError()
2213
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002214 def SubmitIssue(self, wait_for_merge=True):
2215 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2216 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002217
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002218 def _GetChangeDetail(self, options=None, issue=None):
2219 options = options or []
2220 issue = issue or self.GetIssue()
2221 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002222 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2223 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002224
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002225 def CMDLand(self, force, bypass_hooks, verbose):
2226 if git_common.is_dirty_git_tree('land'):
2227 return 1
2228 differs = True
2229 last_upload = RunGit(['config',
2230 'branch.%s.gerritsquashhash' % self.GetBranch()],
2231 error_ok=True).strip()
2232 # Note: git diff outputs nothing if there is no diff.
2233 if not last_upload or RunGit(['diff', last_upload]).strip():
2234 print('WARNING: some changes from local branch haven\'t been uploaded')
2235 else:
2236 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2237 if detail['current_revision'] == last_upload:
2238 differs = False
2239 else:
2240 print('WARNING: local branch contents differ from latest uploaded '
2241 'patchset')
2242 if differs:
2243 if not force:
2244 ask_for_data(
2245 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2246 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2247 elif not bypass_hooks:
2248 hook_results = self.RunHook(
2249 committing=True,
2250 may_prompt=not force,
2251 verbose=verbose,
2252 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2253 if not hook_results.should_continue():
2254 return 1
2255
2256 self.SubmitIssue(wait_for_merge=True)
2257 print('Issue %s has been submitted.' % self.GetIssueURL())
2258 return 0
2259
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002260 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2261 directory):
2262 assert not reject
2263 assert not nocommit
2264 assert not directory
2265 assert parsed_issue_arg.valid
2266
2267 self._changelist.issue = parsed_issue_arg.issue
2268
2269 if parsed_issue_arg.hostname:
2270 self._gerrit_host = parsed_issue_arg.hostname
2271 self._gerrit_server = 'https://%s' % self._gerrit_host
2272
2273 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2274
2275 if not parsed_issue_arg.patchset:
2276 # Use current revision by default.
2277 revision_info = detail['revisions'][detail['current_revision']]
2278 patchset = int(revision_info['_number'])
2279 else:
2280 patchset = parsed_issue_arg.patchset
2281 for revision_info in detail['revisions'].itervalues():
2282 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2283 break
2284 else:
2285 DieWithError('Couldn\'t find patchset %i in issue %i' %
2286 (parsed_issue_arg.patchset, self.GetIssue()))
2287
2288 fetch_info = revision_info['fetch']['http']
2289 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2290 RunGit(['cherry-pick', 'FETCH_HEAD'])
2291 self.SetIssue(self.GetIssue())
2292 self.SetPatchset(patchset)
2293 print('Committed patch for issue %i pathset %i locally' %
2294 (self.GetIssue(), self.GetPatchset()))
2295 return 0
2296
2297 @staticmethod
2298 def ParseIssueURL(parsed_url):
2299 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2300 return None
2301 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2302 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2303 # Short urls like https://domain/<issue_number> can be used, but don't allow
2304 # specifying the patchset (you'd 404), but we allow that here.
2305 if parsed_url.path == '/':
2306 part = parsed_url.fragment
2307 else:
2308 part = parsed_url.path
2309 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2310 if match:
2311 return _ParsedIssueNumberArgument(
2312 issue=int(match.group(2)),
2313 patchset=int(match.group(4)) if match.group(4) else None,
2314 hostname=parsed_url.netloc)
2315 return None
2316
tandrii16e0b4e2016-06-07 10:34:28 -07002317 def _GerritCommitMsgHookCheck(self, offer_removal):
2318 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2319 if not os.path.exists(hook):
2320 return
2321 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2322 # custom developer made one.
2323 data = gclient_utils.FileRead(hook)
2324 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2325 return
2326 print('Warning: you have Gerrit commit-msg hook installed.\n'
2327 'It is not neccessary for uploading with git cl in squash mode, '
2328 'and may interfere with it in subtle ways.\n'
2329 'We recommend you remove the commit-msg hook.')
2330 if offer_removal:
2331 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2332 if reply.lower().startswith('y'):
2333 gclient_utils.rm_file_or_tree(hook)
2334 print('Gerrit commit-msg hook removed.')
2335 else:
2336 print('OK, will keep Gerrit commit-msg hook in place.')
2337
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002338 def CMDUploadChange(self, options, args, change):
2339 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002340 if options.squash and options.no_squash:
2341 DieWithError('Can only use one of --squash or --no-squash')
tandrii26f3e4e2016-06-10 08:37:04 -07002342 # TODO(tandrii): remove this by June 20.
2343 if (RunGit(['config', '--bool', 'gerrit.squash-uploads'],
2344 error_ok=True).strip() != 'false' and not options.squash and
2345 not options.no_squash):
2346 print('\n\nHi! You are using git cl upload in --no-squash mode.\n'
2347 'Chrome infrastructure wants to make --squash the default.\n'
2348 'To ensure that --no-squash is still the default for YOU do:\n'
2349 ' git config --bool gerrit.squash-uploads false\n'
2350 'See https://goo.gl/dnK2gV (use chromium.org account!) and '
2351 'let us know what you think. Thanks!\n'
2352 'BUG: http://crbug.com/611892\n\n')
2353
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002354 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2355 not options.no_squash)
tandrii26f3e4e2016-06-10 08:37:04 -07002356
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002357 # We assume the remote called "origin" is the one we want.
2358 # It is probably not worthwhile to support different workflows.
2359 gerrit_remote = 'origin'
2360
2361 remote, remote_branch = self.GetRemoteBranch()
2362 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2363 pending_prefix='')
2364
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002365 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002366 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002367 if not self.GetIssue():
2368 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2369 # with shadow branch, which used to contain change-id for a given
2370 # branch, using which we can fetch actual issue number and set it as the
2371 # property of the branch, which is the new way.
2372 message = RunGitSilent([
2373 'show', '--format=%B', '-s',
2374 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2375 if message:
2376 change_ids = git_footers.get_footer_change_id(message.strip())
2377 if change_ids and len(change_ids) == 1:
2378 details = self._GetChangeDetail(issue=change_ids[0])
2379 if details:
2380 print('WARNING: found old upload in branch git_cl_uploads/%s '
2381 'corresponding to issue %s' %
2382 (self.GetBranch(), details['_number']))
2383 self.SetIssue(details['_number'])
2384 if not self.GetIssue():
2385 DieWithError(
2386 '\n' # For readability of the blob below.
2387 'Found old upload in branch git_cl_uploads/%s, '
2388 'but failed to find corresponding Gerrit issue.\n'
2389 'If you know the issue number, set it manually first:\n'
2390 ' git cl issue 123456\n'
2391 'If you intended to upload this CL as new issue, '
2392 'just delete or rename the old upload branch:\n'
2393 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2394 'After that, please run git cl upload again.' %
2395 tuple([self.GetBranch()] * 3))
2396 # End of backwards compatability.
2397
2398 if self.GetIssue():
2399 # Try to get the message from a previous upload.
2400 message = self.GetDescription()
2401 if not message:
2402 DieWithError(
2403 'failed to fetch description from current Gerrit issue %d\n'
2404 '%s' % (self.GetIssue(), self.GetIssueURL()))
2405 change_id = self._GetChangeDetail()['change_id']
2406 while True:
2407 footer_change_ids = git_footers.get_footer_change_id(message)
2408 if footer_change_ids == [change_id]:
2409 break
2410 if not footer_change_ids:
2411 message = git_footers.add_footer_change_id(message, change_id)
2412 print('WARNING: appended missing Change-Id to issue description')
2413 continue
2414 # There is already a valid footer but with different or several ids.
2415 # Doing this automatically is non-trivial as we don't want to lose
2416 # existing other footers, yet we want to append just 1 desired
2417 # Change-Id. Thus, just create a new footer, but let user verify the
2418 # new description.
2419 message = '%s\n\nChange-Id: %s' % (message, change_id)
2420 print(
2421 'WARNING: issue %s has Change-Id footer(s):\n'
2422 ' %s\n'
2423 'but issue has Change-Id %s, according to Gerrit.\n'
2424 'Please, check the proposed correction to the description, '
2425 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2426 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2427 change_id))
2428 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2429 if not options.force:
2430 change_desc = ChangeDescription(message)
2431 change_desc.prompt()
2432 message = change_desc.description
2433 if not message:
2434 DieWithError("Description is empty. Aborting...")
2435 # Continue the while loop.
2436 # Sanity check of this code - we should end up with proper message
2437 # footer.
2438 assert [change_id] == git_footers.get_footer_change_id(message)
2439 change_desc = ChangeDescription(message)
2440 else:
2441 change_desc = ChangeDescription(
2442 options.message or CreateDescriptionFromLog(args))
2443 if not options.force:
2444 change_desc.prompt()
2445 if not change_desc.description:
2446 DieWithError("Description is empty. Aborting...")
2447 message = change_desc.description
2448 change_ids = git_footers.get_footer_change_id(message)
2449 if len(change_ids) > 1:
2450 DieWithError('too many Change-Id footers, at most 1 allowed.')
2451 if not change_ids:
2452 # Generate the Change-Id automatically.
2453 message = git_footers.add_footer_change_id(
2454 message, GenerateGerritChangeId(message))
2455 change_desc.set_description(message)
2456 change_ids = git_footers.get_footer_change_id(message)
2457 assert len(change_ids) == 1
2458 change_id = change_ids[0]
2459
2460 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2461 if remote is '.':
2462 # If our upstream branch is local, we base our squashed commit on its
2463 # squashed version.
2464 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2465 # Check the squashed hash of the parent.
2466 parent = RunGit(['config',
2467 'branch.%s.gerritsquashhash' % upstream_branch_name],
2468 error_ok=True).strip()
2469 # Verify that the upstream branch has been uploaded too, otherwise
2470 # Gerrit will create additional CLs when uploading.
2471 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2472 RunGitSilent(['rev-parse', parent + ':'])):
2473 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2474 DieWithError(
2475 'Upload upstream branch %s first.\n'
2476 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2477 'version of depot_tools. If so, then re-upload it with:\n'
2478 ' git cl upload --squash\n' % upstream_branch_name)
2479 else:
2480 parent = self.GetCommonAncestorWithUpstream()
2481
2482 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2483 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2484 '-m', message]).strip()
2485 else:
2486 change_desc = ChangeDescription(
2487 options.message or CreateDescriptionFromLog(args))
2488 if not change_desc.description:
2489 DieWithError("Description is empty. Aborting...")
2490
2491 if not git_footers.get_footer_change_id(change_desc.description):
2492 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002493 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2494 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002495 ref_to_push = 'HEAD'
2496 parent = '%s/%s' % (gerrit_remote, branch)
2497 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2498
2499 assert change_desc
2500 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2501 ref_to_push)]).splitlines()
2502 if len(commits) > 1:
2503 print('WARNING: This will upload %d commits. Run the following command '
2504 'to see which commits will be uploaded: ' % len(commits))
2505 print('git log %s..%s' % (parent, ref_to_push))
2506 print('You can also use `git squash-branch` to squash these into a '
2507 'single commit.')
2508 ask_for_data('About to upload; enter to confirm.')
2509
2510 if options.reviewers or options.tbr_owners:
2511 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2512 change)
2513
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002514 # Extra options that can be specified at push time. Doc:
2515 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2516 refspec_opts = []
2517 if options.title:
2518 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2519 # reverse on its side.
2520 if '_' in options.title:
2521 print('WARNING: underscores in title will be converted to spaces.')
2522 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2523
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002524 if options.send_mail:
2525 if not change_desc.get_reviewers():
2526 DieWithError('Must specify reviewers to send email.')
2527 refspec_opts.append('notify=ALL')
2528 else:
2529 refspec_opts.append('notify=NONE')
2530
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002531 cc = self.GetCCList().split(',')
2532 if options.cc:
2533 cc.extend(options.cc)
2534 cc = filter(None, cc)
2535 if cc:
tandrii@chromium.org074c2af2016-06-03 23:18:40 +00002536 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002537
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002538 if change_desc.get_reviewers():
2539 refspec_opts.extend('r=' + email.strip()
2540 for email in change_desc.get_reviewers())
2541
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002542 refspec_suffix = ''
2543 if refspec_opts:
2544 refspec_suffix = '%' + ','.join(refspec_opts)
2545 assert ' ' not in refspec_suffix, (
2546 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002547 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002548
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002549 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002550 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002551 print_stdout=True,
2552 # Flush after every line: useful for seeing progress when running as
2553 # recipe.
2554 filter_fn=lambda _: sys.stdout.flush())
2555
2556 if options.squash:
2557 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2558 change_numbers = [m.group(1)
2559 for m in map(regex.match, push_stdout.splitlines())
2560 if m]
2561 if len(change_numbers) != 1:
2562 DieWithError(
2563 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2564 'Change-Id: %s') % (len(change_numbers), change_id))
2565 self.SetIssue(change_numbers[0])
2566 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2567 ref_to_push])
2568 return 0
2569
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002570 def _AddChangeIdToCommitMessage(self, options, args):
2571 """Re-commits using the current message, assumes the commit hook is in
2572 place.
2573 """
2574 log_desc = options.message or CreateDescriptionFromLog(args)
2575 git_command = ['commit', '--amend', '-m', log_desc]
2576 RunGit(git_command)
2577 new_log_desc = CreateDescriptionFromLog(args)
2578 if git_footers.get_footer_change_id(new_log_desc):
2579 print 'git-cl: Added Change-Id to commit message.'
2580 return new_log_desc
2581 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002582 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002583
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002584 def SetCQState(self, new_state):
2585 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2586 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2587 # self-discovery of label config for this CL using REST API.
2588 vote_map = {
2589 _CQState.NONE: 0,
2590 _CQState.DRY_RUN: 1,
2591 _CQState.COMMIT : 2,
2592 }
2593 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2594 labels={'Commit-Queue': vote_map[new_state]})
2595
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002596
2597_CODEREVIEW_IMPLEMENTATIONS = {
2598 'rietveld': _RietveldChangelistImpl,
2599 'gerrit': _GerritChangelistImpl,
2600}
2601
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002602
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002603def _add_codereview_select_options(parser):
2604 """Appends --gerrit and --rietveld options to force specific codereview."""
2605 parser.codereview_group = optparse.OptionGroup(
2606 parser, 'EXPERIMENTAL! Codereview override options')
2607 parser.add_option_group(parser.codereview_group)
2608 parser.codereview_group.add_option(
2609 '--gerrit', action='store_true',
2610 help='Force the use of Gerrit for codereview')
2611 parser.codereview_group.add_option(
2612 '--rietveld', action='store_true',
2613 help='Force the use of Rietveld for codereview')
2614
2615
2616def _process_codereview_select_options(parser, options):
2617 if options.gerrit and options.rietveld:
2618 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2619 options.forced_codereview = None
2620 if options.gerrit:
2621 options.forced_codereview = 'gerrit'
2622 elif options.rietveld:
2623 options.forced_codereview = 'rietveld'
2624
2625
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002626class ChangeDescription(object):
2627 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002628 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002629 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002630
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002631 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002632 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002633
agable@chromium.org42c20792013-09-12 17:34:49 +00002634 @property # www.logilab.org/ticket/89786
2635 def description(self): # pylint: disable=E0202
2636 return '\n'.join(self._description_lines)
2637
2638 def set_description(self, desc):
2639 if isinstance(desc, basestring):
2640 lines = desc.splitlines()
2641 else:
2642 lines = [line.rstrip() for line in desc]
2643 while lines and not lines[0]:
2644 lines.pop(0)
2645 while lines and not lines[-1]:
2646 lines.pop(-1)
2647 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002648
piman@chromium.org336f9122014-09-04 02:16:55 +00002649 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002650 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002651 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002652 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002653 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002654 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002655
agable@chromium.org42c20792013-09-12 17:34:49 +00002656 # Get the set of R= and TBR= lines and remove them from the desciption.
2657 regexp = re.compile(self.R_LINE)
2658 matches = [regexp.match(line) for line in self._description_lines]
2659 new_desc = [l for i, l in enumerate(self._description_lines)
2660 if not matches[i]]
2661 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002662
agable@chromium.org42c20792013-09-12 17:34:49 +00002663 # Construct new unified R= and TBR= lines.
2664 r_names = []
2665 tbr_names = []
2666 for match in matches:
2667 if not match:
2668 continue
2669 people = cleanup_list([match.group(2).strip()])
2670 if match.group(1) == 'TBR':
2671 tbr_names.extend(people)
2672 else:
2673 r_names.extend(people)
2674 for name in r_names:
2675 if name not in reviewers:
2676 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002677 if add_owners_tbr:
2678 owners_db = owners.Database(change.RepositoryRoot(),
2679 fopen=file, os_path=os.path, glob=glob.glob)
2680 all_reviewers = set(tbr_names + reviewers)
2681 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2682 all_reviewers)
2683 tbr_names.extend(owners_db.reviewers_for(missing_files,
2684 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002685 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2686 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2687
2688 # Put the new lines in the description where the old first R= line was.
2689 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2690 if 0 <= line_loc < len(self._description_lines):
2691 if new_tbr_line:
2692 self._description_lines.insert(line_loc, new_tbr_line)
2693 if new_r_line:
2694 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002695 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002696 if new_r_line:
2697 self.append_footer(new_r_line)
2698 if new_tbr_line:
2699 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002700
2701 def prompt(self):
2702 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002703 self.set_description([
2704 '# Enter a description of the change.',
2705 '# This will be displayed on the codereview site.',
2706 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002707 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002708 '--------------------',
2709 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002710
agable@chromium.org42c20792013-09-12 17:34:49 +00002711 regexp = re.compile(self.BUG_LINE)
2712 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002713 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002714 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002715 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002716 if not content:
2717 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002718 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002719
2720 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002721 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2722 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002723 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002724 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002725
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002726 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002727 """Adds a footer line to the description.
2728
2729 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2730 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2731 that Gerrit footers are always at the end.
2732 """
2733 parsed_footer_line = git_footers.parse_footer(line)
2734 if parsed_footer_line:
2735 # Line is a gerrit footer in the form: Footer-Key: any value.
2736 # Thus, must be appended observing Gerrit footer rules.
2737 self.set_description(
2738 git_footers.add_footer(self.description,
2739 key=parsed_footer_line[0],
2740 value=parsed_footer_line[1]))
2741 return
2742
2743 if not self._description_lines:
2744 self._description_lines.append(line)
2745 return
2746
2747 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2748 if gerrit_footers:
2749 # git_footers.split_footers ensures that there is an empty line before
2750 # actual (gerrit) footers, if any. We have to keep it that way.
2751 assert top_lines and top_lines[-1] == ''
2752 top_lines, separator = top_lines[:-1], top_lines[-1:]
2753 else:
2754 separator = [] # No need for separator if there are no gerrit_footers.
2755
2756 prev_line = top_lines[-1] if top_lines else ''
2757 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2758 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2759 top_lines.append('')
2760 top_lines.append(line)
2761 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002762
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002763 def get_reviewers(self):
2764 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002765 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2766 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002767 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002768
2769
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002770def get_approving_reviewers(props):
2771 """Retrieves the reviewers that approved a CL from the issue properties with
2772 messages.
2773
2774 Note that the list may contain reviewers that are not committer, thus are not
2775 considered by the CQ.
2776 """
2777 return sorted(
2778 set(
2779 message['sender']
2780 for message in props['messages']
2781 if message['approval'] and message['sender'] in props['reviewers']
2782 )
2783 )
2784
2785
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002786def FindCodereviewSettingsFile(filename='codereview.settings'):
2787 """Finds the given file starting in the cwd and going up.
2788
2789 Only looks up to the top of the repository unless an
2790 'inherit-review-settings-ok' file exists in the root of the repository.
2791 """
2792 inherit_ok_file = 'inherit-review-settings-ok'
2793 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002794 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002795 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2796 root = '/'
2797 while True:
2798 if filename in os.listdir(cwd):
2799 if os.path.isfile(os.path.join(cwd, filename)):
2800 return open(os.path.join(cwd, filename))
2801 if cwd == root:
2802 break
2803 cwd = os.path.dirname(cwd)
2804
2805
2806def LoadCodereviewSettingsFromFile(fileobj):
2807 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002808 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002810 def SetProperty(name, setting, unset_error_ok=False):
2811 fullname = 'rietveld.' + name
2812 if setting in keyvals:
2813 RunGit(['config', fullname, keyvals[setting]])
2814 else:
2815 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2816
2817 SetProperty('server', 'CODE_REVIEW_SERVER')
2818 # Only server setting is required. Other settings can be absent.
2819 # In that case, we ignore errors raised during option deletion attempt.
2820 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002821 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002822 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2823 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002824 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002825 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002826 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2827 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002828 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002829 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002830 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002831 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2832 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002833
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002834 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002835 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002836
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002837 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2838 RunGit(['config', 'gerrit.squash-uploads',
2839 keyvals['GERRIT_SQUASH_UPLOADS']])
2840
tandrii@chromium.org28253532016-04-14 13:46:56 +00002841 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002842 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002843 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002845 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2846 #should be of the form
2847 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2848 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2849 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2850 keyvals['ORIGIN_URL_CONFIG']])
2851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002853def urlretrieve(source, destination):
2854 """urllib is broken for SSL connections via a proxy therefore we
2855 can't use urllib.urlretrieve()."""
2856 with open(destination, 'w') as f:
2857 f.write(urllib2.urlopen(source).read())
2858
2859
ukai@chromium.org712d6102013-11-27 00:52:58 +00002860def hasSheBang(fname):
2861 """Checks fname is a #! script."""
2862 with open(fname) as f:
2863 return f.read(2).startswith('#!')
2864
2865
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002866# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2867def DownloadHooks(*args, **kwargs):
2868 pass
2869
2870
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002871def DownloadGerritHook(force):
2872 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002873
2874 Args:
2875 force: True to update hooks. False to install hooks if not present.
2876 """
2877 if not settings.GetIsGerrit():
2878 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002879 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002880 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2881 if not os.access(dst, os.X_OK):
2882 if os.path.exists(dst):
2883 if not force:
2884 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002885 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002886 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002887 if not hasSheBang(dst):
2888 DieWithError('Not a script: %s\n'
2889 'You need to download from\n%s\n'
2890 'into .git/hooks/commit-msg and '
2891 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002892 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2893 except Exception:
2894 if os.path.exists(dst):
2895 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002896 DieWithError('\nFailed to download hooks.\n'
2897 'You need to download from\n%s\n'
2898 'into .git/hooks/commit-msg and '
2899 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002900
2901
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002902
2903def GetRietveldCodereviewSettingsInteractively():
2904 """Prompt the user for settings."""
2905 server = settings.GetDefaultServerUrl(error_ok=True)
2906 prompt = 'Rietveld server (host[:port])'
2907 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2908 newserver = ask_for_data(prompt + ':')
2909 if not server and not newserver:
2910 newserver = DEFAULT_SERVER
2911 if newserver:
2912 newserver = gclient_utils.UpgradeToHttps(newserver)
2913 if newserver != server:
2914 RunGit(['config', 'rietveld.server', newserver])
2915
2916 def SetProperty(initial, caption, name, is_url):
2917 prompt = caption
2918 if initial:
2919 prompt += ' ("x" to clear) [%s]' % initial
2920 new_val = ask_for_data(prompt + ':')
2921 if new_val == 'x':
2922 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2923 elif new_val:
2924 if is_url:
2925 new_val = gclient_utils.UpgradeToHttps(new_val)
2926 if new_val != initial:
2927 RunGit(['config', 'rietveld.' + name, new_val])
2928
2929 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2930 SetProperty(settings.GetDefaultPrivateFlag(),
2931 'Private flag (rietveld only)', 'private', False)
2932 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2933 'tree-status-url', False)
2934 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2935 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2936 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2937 'run-post-upload-hook', False)
2938
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002939@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002940def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002941 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002942
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002943 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002944 'For Gerrit, see http://crbug.com/603116.')
2945 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002946 parser.add_option('--activate-update', action='store_true',
2947 help='activate auto-updating [rietveld] section in '
2948 '.git/config')
2949 parser.add_option('--deactivate-update', action='store_true',
2950 help='deactivate auto-updating [rietveld] section in '
2951 '.git/config')
2952 options, args = parser.parse_args(args)
2953
2954 if options.deactivate_update:
2955 RunGit(['config', 'rietveld.autoupdate', 'false'])
2956 return
2957
2958 if options.activate_update:
2959 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2960 return
2961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002962 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002963 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002964 return 0
2965
2966 url = args[0]
2967 if not url.endswith('codereview.settings'):
2968 url = os.path.join(url, 'codereview.settings')
2969
2970 # Load code review settings and download hooks (if available).
2971 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2972 return 0
2973
2974
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002975def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002976 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002977 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2978 branch = ShortBranchName(branchref)
2979 _, args = parser.parse_args(args)
2980 if not args:
2981 print("Current base-url:")
2982 return RunGit(['config', 'branch.%s.base-url' % branch],
2983 error_ok=False).strip()
2984 else:
2985 print("Setting base-url to %s" % args[0])
2986 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2987 error_ok=False).strip()
2988
2989
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002990def color_for_status(status):
2991 """Maps a Changelist status to color, for CMDstatus and other tools."""
2992 return {
2993 'unsent': Fore.RED,
2994 'waiting': Fore.BLUE,
2995 'reply': Fore.YELLOW,
2996 'lgtm': Fore.GREEN,
2997 'commit': Fore.MAGENTA,
2998 'closed': Fore.CYAN,
2999 'error': Fore.WHITE,
3000 }.get(status, Fore.WHITE)
3001
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003002
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003003def get_cl_statuses(changes, fine_grained, max_processes=None):
3004 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003005
3006 If fine_grained is true, this will fetch CL statuses from the server.
3007 Otherwise, simply indicate if there's a matching url for the given branches.
3008
3009 If max_processes is specified, it is used as the maximum number of processes
3010 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3011 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003012
3013 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003014 """
3015 # Silence upload.py otherwise it becomes unwieldly.
3016 upload.verbosity = 0
3017
3018 if fine_grained:
3019 # Process one branch synchronously to work through authentication, then
3020 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003021 if changes:
3022 fetch = lambda cl: (cl, cl.GetStatus())
3023 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003024
kmarshall3bff56b2016-06-06 18:31:47 -07003025 if not changes:
3026 # Exit early if there was only one branch to fetch.
3027 return
3028
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003029 changes_to_fetch = changes[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003030 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003031 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003032 if max_processes is not None
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003033 else len(changes_to_fetch))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003034
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003035 fetched_cls = set()
3036 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003037 while True:
3038 try:
3039 row = it.next(timeout=5)
3040 except multiprocessing.TimeoutError:
3041 break
3042
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003043 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003044 yield row
3045
3046 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003047 for cl in set(changes_to_fetch) - fetched_cls:
3048 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003049
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003050 else:
3051 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003052 for cl in changes:
3053 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003054
rmistry@google.com2dd99862015-06-22 12:22:18 +00003055
3056def upload_branch_deps(cl, args):
3057 """Uploads CLs of local branches that are dependents of the current branch.
3058
3059 If the local branch dependency tree looks like:
3060 test1 -> test2.1 -> test3.1
3061 -> test3.2
3062 -> test2.2 -> test3.3
3063
3064 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3065 run on the dependent branches in this order:
3066 test2.1, test3.1, test3.2, test2.2, test3.3
3067
3068 Note: This function does not rebase your local dependent branches. Use it when
3069 you make a change to the parent branch that will not conflict with its
3070 dependent branches, and you would like their dependencies updated in
3071 Rietveld.
3072 """
3073 if git_common.is_dirty_git_tree('upload-branch-deps'):
3074 return 1
3075
3076 root_branch = cl.GetBranch()
3077 if root_branch is None:
3078 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3079 'Get on a branch!')
3080 if not cl.GetIssue() or not cl.GetPatchset():
3081 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3082 'patchset dependencies without an uploaded CL.')
3083
3084 branches = RunGit(['for-each-ref',
3085 '--format=%(refname:short) %(upstream:short)',
3086 'refs/heads'])
3087 if not branches:
3088 print('No local branches found.')
3089 return 0
3090
3091 # Create a dictionary of all local branches to the branches that are dependent
3092 # on it.
3093 tracked_to_dependents = collections.defaultdict(list)
3094 for b in branches.splitlines():
3095 tokens = b.split()
3096 if len(tokens) == 2:
3097 branch_name, tracked = tokens
3098 tracked_to_dependents[tracked].append(branch_name)
3099
3100 print
3101 print 'The dependent local branches of %s are:' % root_branch
3102 dependents = []
3103 def traverse_dependents_preorder(branch, padding=''):
3104 dependents_to_process = tracked_to_dependents.get(branch, [])
3105 padding += ' '
3106 for dependent in dependents_to_process:
3107 print '%s%s' % (padding, dependent)
3108 dependents.append(dependent)
3109 traverse_dependents_preorder(dependent, padding)
3110 traverse_dependents_preorder(root_branch)
3111 print
3112
3113 if not dependents:
3114 print 'There are no dependent local branches for %s' % root_branch
3115 return 0
3116
3117 print ('This command will checkout all dependent branches and run '
3118 '"git cl upload".')
3119 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3120
andybons@chromium.org962f9462016-02-03 20:00:42 +00003121 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003122 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003123 args.extend(['-t', 'Updated patchset dependency'])
3124
rmistry@google.com2dd99862015-06-22 12:22:18 +00003125 # Record all dependents that failed to upload.
3126 failures = {}
3127 # Go through all dependents, checkout the branch and upload.
3128 try:
3129 for dependent_branch in dependents:
3130 print
3131 print '--------------------------------------'
3132 print 'Running "git cl upload" from %s:' % dependent_branch
3133 RunGit(['checkout', '-q', dependent_branch])
3134 print
3135 try:
3136 if CMDupload(OptionParser(), args) != 0:
3137 print 'Upload failed for %s!' % dependent_branch
3138 failures[dependent_branch] = 1
3139 except: # pylint: disable=W0702
3140 failures[dependent_branch] = 1
3141 print
3142 finally:
3143 # Swap back to the original root branch.
3144 RunGit(['checkout', '-q', root_branch])
3145
3146 print
3147 print 'Upload complete for dependent branches!'
3148 for dependent_branch in dependents:
3149 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3150 print ' %s : %s' % (dependent_branch, upload_status)
3151 print
3152
3153 return 0
3154
3155
kmarshall3bff56b2016-06-06 18:31:47 -07003156def CMDarchive(parser, args):
3157 """Archives and deletes branches associated with closed changelists."""
3158 parser.add_option(
3159 '-j', '--maxjobs', action='store', type=int,
3160 help='The maximum number of jobs to use when retrieving review status')
3161 parser.add_option(
3162 '-f', '--force', action='store_true',
3163 help='Bypasses the confirmation prompt.')
3164
3165 auth.add_auth_options(parser)
3166 options, args = parser.parse_args(args)
3167 if args:
3168 parser.error('Unsupported args: %s' % ' '.join(args))
3169 auth_config = auth.extract_auth_config_from_options(options)
3170
3171 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3172 if not branches:
3173 return 0
3174
3175 print 'Finding all branches associated with closed issues...'
3176 changes = [Changelist(branchref=b, auth_config=auth_config)
3177 for b in branches.splitlines()]
3178 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3179 statuses = get_cl_statuses(changes,
3180 fine_grained=True,
3181 max_processes=options.maxjobs)
3182 proposal = [(cl.GetBranch(),
3183 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3184 for cl, status in statuses
3185 if status == 'closed']
3186 proposal.sort()
3187
3188 if not proposal:
3189 print 'No branches with closed codereview issues found.'
3190 return 0
3191
3192 current_branch = GetCurrentBranch()
3193
3194 print '\nBranches with closed issues that will be archived:\n'
3195 print '%*s | %s' % (alignment, 'Branch name', 'Archival tag name')
3196 for next_item in proposal:
3197 print '%*s %s' % (alignment, next_item[0], next_item[1])
3198
3199 if any(branch == current_branch for branch, _ in proposal):
3200 print('You are currently on a branch \'%s\' which is associated with a '
3201 'closed codereview issue, so archive cannot proceed. Please '
3202 'checkout another branch and run this command again.' %
3203 current_branch)
3204 return 1
3205
3206 if not options.force:
3207 if ask_for_data('\nProceed with deletion (Y/N)? ').lower() != 'y':
3208 print 'Aborted.'
3209 return 1
3210
3211 for branch, tagname in proposal:
3212 RunGit(['tag', tagname, branch])
3213 RunGit(['branch', '-D', branch])
3214 print '\nJob\'s done!'
3215
3216 return 0
3217
3218
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003220 """Show status of changelists.
3221
3222 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003223 - Red not sent for review or broken
3224 - Blue waiting for review
3225 - Yellow waiting for you to reply to review
3226 - Green LGTM'ed
3227 - Magenta in the commit queue
3228 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003229
3230 Also see 'git cl comments'.
3231 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003232 parser.add_option('--field',
3233 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003234 parser.add_option('-f', '--fast', action='store_true',
3235 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003236 parser.add_option(
3237 '-j', '--maxjobs', action='store', type=int,
3238 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003239
3240 auth.add_auth_options(parser)
3241 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003242 if args:
3243 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003244 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003247 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003248 if options.field.startswith('desc'):
3249 print cl.GetDescription()
3250 elif options.field == 'id':
3251 issueid = cl.GetIssue()
3252 if issueid:
3253 print issueid
3254 elif options.field == 'patch':
3255 patchset = cl.GetPatchset()
3256 if patchset:
3257 print patchset
3258 elif options.field == 'url':
3259 url = cl.GetIssueURL()
3260 if url:
3261 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003262 return 0
3263
3264 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3265 if not branches:
3266 print('No local branch found.')
3267 return 0
3268
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003269 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003270 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003271 for b in branches.splitlines()]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003272 print 'Branches associated with reviews:'
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003273 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003274 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003275 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003276
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003277 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003278 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3279 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3280 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003281 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003282 c, status = output.next()
3283 branch_statuses[c.GetBranch()] = status
3284 status = branch_statuses.pop(branch)
3285 url = cl.GetIssueURL()
3286 if url and (not status or status == 'error'):
3287 # The issue probably doesn't exist anymore.
3288 url += ' (broken)'
3289
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003290 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003291 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003292 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003293 color = ''
3294 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003295 status_str = '(%s)' % status if status else ''
3296 print ' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003297 alignment, ShortBranchName(branch), color, url,
3298 status_str, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003299
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003300 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003301 print
3302 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003303 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003304 if not cl.GetIssue():
3305 print 'No issue assigned.'
3306 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003307 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003308 if not options.fast:
3309 print 'Issue description:'
3310 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003311 return 0
3312
3313
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003314def colorize_CMDstatus_doc():
3315 """To be called once in main() to add colors to git cl status help."""
3316 colors = [i for i in dir(Fore) if i[0].isupper()]
3317
3318 def colorize_line(line):
3319 for color in colors:
3320 if color in line.upper():
3321 # Extract whitespaces first and the leading '-'.
3322 indent = len(line) - len(line.lstrip(' ')) + 1
3323 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3324 return line
3325
3326 lines = CMDstatus.__doc__.splitlines()
3327 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3328
3329
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003330@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003331def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003332 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003333
3334 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003335 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003336 parser.add_option('-r', '--reverse', action='store_true',
3337 help='Lookup the branch(es) for the specified issues. If '
3338 'no issues are specified, all branches with mapped '
3339 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003340 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003341 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003342 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003343
dnj@chromium.org406c4402015-03-03 17:22:28 +00003344 if options.reverse:
3345 branches = RunGit(['for-each-ref', 'refs/heads',
3346 '--format=%(refname:short)']).splitlines()
3347
3348 # Reverse issue lookup.
3349 issue_branch_map = {}
3350 for branch in branches:
3351 cl = Changelist(branchref=branch)
3352 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3353 if not args:
3354 args = sorted(issue_branch_map.iterkeys())
3355 for issue in args:
3356 if not issue:
3357 continue
3358 print 'Branch for issue number %s: %s' % (
3359 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3360 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003361 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003362 if len(args) > 0:
3363 try:
3364 issue = int(args[0])
3365 except ValueError:
3366 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003367 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003368 cl.SetIssue(issue)
3369 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003370 return 0
3371
3372
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003373def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003374 """Shows or posts review comments for any changelist."""
3375 parser.add_option('-a', '--add-comment', dest='comment',
3376 help='comment to add to an issue')
3377 parser.add_option('-i', dest='issue',
3378 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003379 parser.add_option('-j', '--json-file',
3380 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003381 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003382 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003383 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003384
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003385 issue = None
3386 if options.issue:
3387 try:
3388 issue = int(options.issue)
3389 except ValueError:
3390 DieWithError('A review issue id is expected to be a number')
3391
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003392 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003393
3394 if options.comment:
3395 cl.AddComment(options.comment)
3396 return 0
3397
3398 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003399 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003400 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003401 summary.append({
3402 'date': message['date'],
3403 'lgtm': False,
3404 'message': message['text'],
3405 'not_lgtm': False,
3406 'sender': message['sender'],
3407 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003408 if message['disapproval']:
3409 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003410 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003411 elif message['approval']:
3412 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003413 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003414 elif message['sender'] == data['owner_email']:
3415 color = Fore.MAGENTA
3416 else:
3417 color = Fore.BLUE
3418 print '\n%s%s %s%s' % (
3419 color, message['date'].split('.', 1)[0], message['sender'],
3420 Fore.RESET)
3421 if message['text'].strip():
3422 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003423 if options.json_file:
3424 with open(options.json_file, 'wb') as f:
3425 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003426 return 0
3427
3428
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003429@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003430def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003431 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003432 parser.add_option('-d', '--display', action='store_true',
3433 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003434 parser.add_option('-n', '--new-description',
3435 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003436
3437 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003438 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003439 options, args = parser.parse_args(args)
3440 _process_codereview_select_options(parser, options)
3441
3442 target_issue = None
3443 if len(args) > 0:
3444 issue_arg = ParseIssueNumberArgument(args[0])
3445 if not issue_arg.valid:
3446 parser.print_help()
3447 return 1
3448 target_issue = issue_arg.issue
3449
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003450 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003451
3452 cl = Changelist(
3453 auth_config=auth_config, issue=target_issue,
3454 codereview=options.forced_codereview)
3455
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003456 if not cl.GetIssue():
3457 DieWithError('This branch has no associated changelist.')
3458 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003459
smut@google.com34fb6b12015-07-13 20:03:26 +00003460 if options.display:
tandrii@chromium.org8c3b4422016-04-27 13:11:18 +00003461 print description.description
smut@google.com34fb6b12015-07-13 20:03:26 +00003462 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003463
3464 if options.new_description:
3465 text = options.new_description
3466 if text == '-':
3467 text = '\n'.join(l.rstrip() for l in sys.stdin)
3468
3469 description.set_description(text)
3470 else:
3471 description.prompt()
3472
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003473 if cl.GetDescription() != description.description:
3474 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003475 return 0
3476
3477
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003478def CreateDescriptionFromLog(args):
3479 """Pulls out the commit log to use as a base for the CL description."""
3480 log_args = []
3481 if len(args) == 1 and not args[0].endswith('.'):
3482 log_args = [args[0] + '..']
3483 elif len(args) == 1 and args[0].endswith('...'):
3484 log_args = [args[0][:-1]]
3485 elif len(args) == 2:
3486 log_args = [args[0] + '..' + args[1]]
3487 else:
3488 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003489 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003490
3491
thestig@chromium.org44202a22014-03-11 19:22:18 +00003492def CMDlint(parser, args):
3493 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003494 parser.add_option('--filter', action='append', metavar='-x,+y',
3495 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003496 auth.add_auth_options(parser)
3497 options, args = parser.parse_args(args)
3498 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003499
3500 # Access to a protected member _XX of a client class
3501 # pylint: disable=W0212
3502 try:
3503 import cpplint
3504 import cpplint_chromium
3505 except ImportError:
3506 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3507 return 1
3508
3509 # Change the current working directory before calling lint so that it
3510 # shows the correct base.
3511 previous_cwd = os.getcwd()
3512 os.chdir(settings.GetRoot())
3513 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003514 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003515 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3516 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003517 if not files:
3518 print "Cannot lint an empty CL"
3519 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003520
3521 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003522 command = args + files
3523 if options.filter:
3524 command = ['--filter=' + ','.join(options.filter)] + command
3525 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003526
3527 white_regex = re.compile(settings.GetLintRegex())
3528 black_regex = re.compile(settings.GetLintIgnoreRegex())
3529 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3530 for filename in filenames:
3531 if white_regex.match(filename):
3532 if black_regex.match(filename):
3533 print "Ignoring file %s" % filename
3534 else:
3535 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3536 extra_check_functions)
3537 else:
3538 print "Skipping file %s" % filename
3539 finally:
3540 os.chdir(previous_cwd)
3541 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3542 if cpplint._cpplint_state.error_count != 0:
3543 return 1
3544 return 0
3545
3546
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003548 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003549 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003550 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003551 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003552 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003553 auth.add_auth_options(parser)
3554 options, args = parser.parse_args(args)
3555 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003556
sbc@chromium.org71437c02015-04-09 19:29:40 +00003557 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003558 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003559 return 1
3560
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003561 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003562 if args:
3563 base_branch = args[0]
3564 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003565 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003566 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003567
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003568 cl.RunHook(
3569 committing=not options.upload,
3570 may_prompt=False,
3571 verbose=options.verbose,
3572 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003573 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003574
3575
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003576def GenerateGerritChangeId(message):
3577 """Returns Ixxxxxx...xxx change id.
3578
3579 Works the same way as
3580 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3581 but can be called on demand on all platforms.
3582
3583 The basic idea is to generate git hash of a state of the tree, original commit
3584 message, author/committer info and timestamps.
3585 """
3586 lines = []
3587 tree_hash = RunGitSilent(['write-tree'])
3588 lines.append('tree %s' % tree_hash.strip())
3589 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3590 if code == 0:
3591 lines.append('parent %s' % parent.strip())
3592 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3593 lines.append('author %s' % author.strip())
3594 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3595 lines.append('committer %s' % committer.strip())
3596 lines.append('')
3597 # Note: Gerrit's commit-hook actually cleans message of some lines and
3598 # whitespace. This code is not doing this, but it clearly won't decrease
3599 # entropy.
3600 lines.append(message)
3601 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3602 stdin='\n'.join(lines))
3603 return 'I%s' % change_hash.strip()
3604
3605
wittman@chromium.org455dc922015-01-26 20:15:50 +00003606def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3607 """Computes the remote branch ref to use for the CL.
3608
3609 Args:
3610 remote (str): The git remote for the CL.
3611 remote_branch (str): The git remote branch for the CL.
3612 target_branch (str): The target branch specified by the user.
3613 pending_prefix (str): The pending prefix from the settings.
3614 """
3615 if not (remote and remote_branch):
3616 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003617
wittman@chromium.org455dc922015-01-26 20:15:50 +00003618 if target_branch:
3619 # Cannonicalize branch references to the equivalent local full symbolic
3620 # refs, which are then translated into the remote full symbolic refs
3621 # below.
3622 if '/' not in target_branch:
3623 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3624 else:
3625 prefix_replacements = (
3626 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3627 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3628 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3629 )
3630 match = None
3631 for regex, replacement in prefix_replacements:
3632 match = re.search(regex, target_branch)
3633 if match:
3634 remote_branch = target_branch.replace(match.group(0), replacement)
3635 break
3636 if not match:
3637 # This is a branch path but not one we recognize; use as-is.
3638 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003639 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3640 # Handle the refs that need to land in different refs.
3641 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003642
wittman@chromium.org455dc922015-01-26 20:15:50 +00003643 # Create the true path to the remote branch.
3644 # Does the following translation:
3645 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3646 # * refs/remotes/origin/master -> refs/heads/master
3647 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3648 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3649 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3650 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3651 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3652 'refs/heads/')
3653 elif remote_branch.startswith('refs/remotes/branch-heads'):
3654 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3655 # If a pending prefix exists then replace refs/ with it.
3656 if pending_prefix:
3657 remote_branch = remote_branch.replace('refs/', pending_prefix)
3658 return remote_branch
3659
3660
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003661def cleanup_list(l):
3662 """Fixes a list so that comma separated items are put as individual items.
3663
3664 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3665 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3666 """
3667 items = sum((i.split(',') for i in l), [])
3668 stripped_items = (i.strip() for i in items)
3669 return sorted(filter(None, stripped_items))
3670
3671
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003672@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003673def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003674 """Uploads the current changelist to codereview.
3675
3676 Can skip dependency patchset uploads for a branch by running:
3677 git config branch.branch_name.skip-deps-uploads True
3678 To unset run:
3679 git config --unset branch.branch_name.skip-deps-uploads
3680 Can also set the above globally by using the --global flag.
3681 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003682 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3683 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003684 parser.add_option('--bypass-watchlists', action='store_true',
3685 dest='bypass_watchlists',
3686 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003687 parser.add_option('-f', action='store_true', dest='force',
3688 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003689 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003690 parser.add_option('-t', dest='title',
3691 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003692 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003693 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003694 help='reviewer email addresses')
3695 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003696 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003697 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003698 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003699 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003700 parser.add_option('--emulate_svn_auto_props',
3701 '--emulate-svn-auto-props',
3702 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003703 dest="emulate_svn_auto_props",
3704 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003705 parser.add_option('-c', '--use-commit-queue', action='store_true',
3706 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003707 parser.add_option('--private', action='store_true',
3708 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003709 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003710 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003711 metavar='TARGET',
3712 help='Apply CL to remote ref TARGET. ' +
3713 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003714 parser.add_option('--squash', action='store_true',
3715 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003716 parser.add_option('--no-squash', action='store_true',
3717 help='Don\'t squash multiple commits into one ' +
3718 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003719 parser.add_option('--email', default=None,
3720 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003721 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3722 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003723 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3724 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003725 help='Send the patchset to do a CQ dry run right after '
3726 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003727 parser.add_option('--dependencies', action='store_true',
3728 help='Uploads CLs of all the local branches that depend on '
3729 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003730
rmistry@google.com2dd99862015-06-22 12:22:18 +00003731 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003732 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003733 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003734 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003735 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003736 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003737 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003738
sbc@chromium.org71437c02015-04-09 19:29:40 +00003739 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003740 return 1
3741
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003742 options.reviewers = cleanup_list(options.reviewers)
3743 options.cc = cleanup_list(options.cc)
3744
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003745 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3746 settings.GetIsGerrit()
3747
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003748 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003749 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003750
3751
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003752def IsSubmoduleMergeCommit(ref):
3753 # When submodules are added to the repo, we expect there to be a single
3754 # non-git-svn merge commit at remote HEAD with a signature comment.
3755 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003756 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003757 return RunGit(cmd) != ''
3758
3759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003760def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003761 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003762
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003763 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3764 upstream and closes the issue automatically and atomically.
3765
3766 Otherwise (in case of Rietveld):
3767 Squashes branch into a single commit.
3768 Updates changelog with metadata (e.g. pointer to review).
3769 Pushes/dcommits the code upstream.
3770 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003771 """
3772 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3773 help='bypass upload presubmit hook')
3774 parser.add_option('-m', dest='message',
3775 help="override review description")
3776 parser.add_option('-f', action='store_true', dest='force',
3777 help="force yes to questions (don't prompt)")
3778 parser.add_option('-c', dest='contributor',
3779 help="external contributor for patch (appended to " +
3780 "description and used as author for git). Should be " +
3781 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003782 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003783 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003784 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003785 auth_config = auth.extract_auth_config_from_options(options)
3786
3787 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003788
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003789 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3790 if cl.IsGerrit():
3791 if options.message:
3792 # This could be implemented, but it requires sending a new patch to
3793 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3794 # Besides, Gerrit has the ability to change the commit message on submit
3795 # automatically, thus there is no need to support this option (so far?).
3796 parser.error('-m MESSAGE option is not supported for Gerrit.')
3797 if options.contributor:
3798 parser.error(
3799 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3800 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3801 'the contributor\'s "name <email>". If you can\'t upload such a '
3802 'commit for review, contact your repository admin and request'
3803 '"Forge-Author" permission.')
3804 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3805 options.verbose)
3806
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003807 current = cl.GetBranch()
3808 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3809 if not settings.GetIsGitSvn() and remote == '.':
3810 print
3811 print 'Attempting to push branch %r into another local branch!' % current
3812 print
3813 print 'Either reparent this branch on top of origin/master:'
3814 print ' git reparent-branch --root'
3815 print
3816 print 'OR run `git rebase-update` if you think the parent branch is already'
3817 print 'committed.'
3818 print
3819 print ' Current parent: %r' % upstream_branch
3820 return 1
3821
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003822 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003823 # Default to merging against our best guess of the upstream branch.
3824 args = [cl.GetUpstreamBranch()]
3825
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003826 if options.contributor:
3827 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3828 print "Please provide contibutor as 'First Last <email@example.com>'"
3829 return 1
3830
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003831 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003832 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003833
sbc@chromium.org71437c02015-04-09 19:29:40 +00003834 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003835 return 1
3836
3837 # This rev-list syntax means "show all commits not in my branch that
3838 # are in base_branch".
3839 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3840 base_branch]).splitlines()
3841 if upstream_commits:
3842 print ('Base branch "%s" has %d commits '
3843 'not in this branch.' % (base_branch, len(upstream_commits)))
3844 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3845 return 1
3846
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003847 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003848 svn_head = None
3849 if cmd == 'dcommit' or base_has_submodules:
3850 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3851 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003852
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003853 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003854 # If the base_head is a submodule merge commit, the first parent of the
3855 # base_head should be a git-svn commit, which is what we're interested in.
3856 base_svn_head = base_branch
3857 if base_has_submodules:
3858 base_svn_head += '^1'
3859
3860 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003861 if extra_commits:
3862 print ('This branch has %d additional commits not upstreamed yet.'
3863 % len(extra_commits.splitlines()))
3864 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3865 'before attempting to %s.' % (base_branch, cmd))
3866 return 1
3867
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003868 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003869 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003870 author = None
3871 if options.contributor:
3872 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003873 hook_results = cl.RunHook(
3874 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003875 may_prompt=not options.force,
3876 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003877 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003878 if not hook_results.should_continue():
3879 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003880
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003881 # Check the tree status if the tree status URL is set.
3882 status = GetTreeStatus()
3883 if 'closed' == status:
3884 print('The tree is closed. Please wait for it to reopen. Use '
3885 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3886 return 1
3887 elif 'unknown' == status:
3888 print('Unable to determine tree status. Please verify manually and '
3889 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3890 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003891
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003892 change_desc = ChangeDescription(options.message)
3893 if not change_desc.description and cl.GetIssue():
3894 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003895
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003896 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003897 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003898 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003899 else:
3900 print 'No description set.'
3901 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3902 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003903
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003904 # Keep a separate copy for the commit message, because the commit message
3905 # contains the link to the Rietveld issue, while the Rietveld message contains
3906 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003907 # Keep a separate copy for the commit message.
3908 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003909 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003910
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003911 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003912 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003913 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003914 # after it. Add a period on a new line to circumvent this. Also add a space
3915 # before the period to make sure that Gitiles continues to correctly resolve
3916 # the URL.
3917 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003918 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003919 commit_desc.append_footer('Patch from %s.' % options.contributor)
3920
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003921 print('Description:')
3922 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003924 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003925 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003926 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003927
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003928 # We want to squash all this branch's commits into one commit with the proper
3929 # description. We do this by doing a "reset --soft" to the base branch (which
3930 # keeps the working copy the same), then dcommitting that. If origin/master
3931 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3932 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003933 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003934 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3935 # Delete the branches if they exist.
3936 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3937 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3938 result = RunGitWithCode(showref_cmd)
3939 if result[0] == 0:
3940 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003941
3942 # We might be in a directory that's present in this branch but not in the
3943 # trunk. Move up to the top of the tree so that git commands that expect a
3944 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003945 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003946 if rel_base_path:
3947 os.chdir(rel_base_path)
3948
3949 # Stuff our change into the merge branch.
3950 # We wrap in a try...finally block so if anything goes wrong,
3951 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003952 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003953 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003954 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003955 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003956 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003957 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003958 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003959 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003960 RunGit(
3961 [
3962 'commit', '--author', options.contributor,
3963 '-m', commit_desc.description,
3964 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003965 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003966 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003967 if base_has_submodules:
3968 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3969 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3970 RunGit(['checkout', CHERRY_PICK_BRANCH])
3971 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003972 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003973 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003974 mirror = settings.GetGitMirror(remote)
3975 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003976 pending_prefix = settings.GetPendingRefPrefix()
3977 if not pending_prefix or branch.startswith(pending_prefix):
3978 # If not using refs/pending/heads/* at all, or target ref is already set
3979 # to pending, then push to the target ref directly.
3980 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003981 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003982 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003983 else:
3984 # Cherry-pick the change on top of pending ref and then push it.
3985 assert branch.startswith('refs/'), branch
3986 assert pending_prefix[-1] == '/', pending_prefix
3987 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003988 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003989 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003990 if retcode == 0:
3991 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992 else:
3993 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003994 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003995 'svn', 'dcommit',
3996 '-C%s' % options.similarity,
3997 '--no-rebase', '--rmdir',
3998 ]
3999 if settings.GetForceHttpsCommitUrl():
4000 # Allow forcing https commit URLs for some projects that don't allow
4001 # committing to http URLs (like Google Code).
4002 remote_url = cl.GetGitSvnRemoteUrl()
4003 if urlparse.urlparse(remote_url).scheme == 'http':
4004 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004005 cmd_args.append('--commit-url=%s' % remote_url)
4006 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004007 if 'Committed r' in output:
4008 revision = re.match(
4009 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4010 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004011 finally:
4012 # And then swap back to the original branch and clean up.
4013 RunGit(['checkout', '-q', cl.GetBranch()])
4014 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004015 if base_has_submodules:
4016 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004017
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004018 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004019 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004020 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004021
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004022 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004023 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004024 try:
4025 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4026 # We set pushed_to_pending to False, since it made it all the way to the
4027 # real ref.
4028 pushed_to_pending = False
4029 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004030 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004031
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004032 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004033 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004034 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004035 if not to_pending:
4036 if viewvc_url and revision:
4037 change_desc.append_footer(
4038 'Committed: %s%s' % (viewvc_url, revision))
4039 elif revision:
4040 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004041 print ('Closing issue '
4042 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004043 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004044 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004045 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004046 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004047 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004048 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004049 if options.bypass_hooks:
4050 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4051 else:
4052 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004053 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004054 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004055
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004056 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004057 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4058 print 'The commit is in the pending queue (%s).' % pending_ref
4059 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00004060 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004061 'footer.' % branch)
4062
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004063 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4064 if os.path.isfile(hook):
4065 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004066
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004067 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004068
4069
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004070def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
4071 print
4072 print 'Waiting for commit to be landed on %s...' % real_ref
4073 print '(If you are impatient, you may Ctrl-C once without harm)'
4074 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4075 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004076 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004077
4078 loop = 0
4079 while True:
4080 sys.stdout.write('fetching (%d)... \r' % loop)
4081 sys.stdout.flush()
4082 loop += 1
4083
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004084 if mirror:
4085 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004086 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4087 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4088 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4089 for commit in commits.splitlines():
4090 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
4091 print 'Found commit on %s' % real_ref
4092 return commit
4093
4094 current_rev = to_rev
4095
4096
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004097def PushToGitPending(remote, pending_ref, upstream_ref):
4098 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4099
4100 Returns:
4101 (retcode of last operation, output log of last operation).
4102 """
4103 assert pending_ref.startswith('refs/'), pending_ref
4104 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4105 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4106 code = 0
4107 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004108 max_attempts = 3
4109 attempts_left = max_attempts
4110 while attempts_left:
4111 if attempts_left != max_attempts:
4112 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
4113 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004114
4115 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004116 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004117 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004118 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004119 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004120 print 'Fetch failed with exit code %d.' % code
4121 if out.strip():
4122 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004123 continue
4124
4125 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004126 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004127 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004128 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004129 if code:
4130 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004131 'Your patch doesn\'t apply cleanly to ref \'%s\', '
4132 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004133 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
4134 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004135 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004136 return code, out
4137
4138 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004139 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004140 code, out = RunGitWithCode(
4141 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4142 if code == 0:
4143 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004144 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004145 return code, out
4146
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004147 print 'Push failed with exit code %d.' % code
4148 if out.strip():
4149 print out.strip()
4150 if IsFatalPushFailure(out):
4151 print (
4152 'Fatal push error. Make sure your .netrc credentials and git '
4153 'user.email are correct and you have push access to the repo.')
4154 return code, out
4155
4156 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004157 return code, out
4158
4159
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004160def IsFatalPushFailure(push_stdout):
4161 """True if retrying push won't help."""
4162 return '(prohibited by Gerrit)' in push_stdout
4163
4164
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004165@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004167 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004168 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004169 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004170 # If it looks like previous commits were mirrored with git-svn.
4171 message = """This repository appears to be a git-svn mirror, but no
4172upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4173 else:
4174 message = """This doesn't appear to be an SVN repository.
4175If your project has a true, writeable git repository, you probably want to run
4176'git cl land' instead.
4177If your project has a git mirror of an upstream SVN master, you probably need
4178to run 'git svn init'.
4179
4180Using the wrong command might cause your commit to appear to succeed, and the
4181review to be closed, without actually landing upstream. If you choose to
4182proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004183 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004184 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185 return SendUpstream(parser, args, 'dcommit')
4186
4187
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004188@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004189def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004190 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004191 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004192 print('This appears to be an SVN repository.')
4193 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004194 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004195 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004196 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004197
4198
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004199@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004200def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004201 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004202 parser.add_option('-b', dest='newbranch',
4203 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004204 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004205 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004206 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4207 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004208 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004209 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004210 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004211 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004212 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004213 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004214
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004215
4216 group = optparse.OptionGroup(
4217 parser,
4218 'Options for continuing work on the current issue uploaded from a '
4219 'different clone (e.g. different machine). Must be used independently '
4220 'from the other options. No issue number should be specified, and the '
4221 'branch must have an issue number associated with it')
4222 group.add_option('--reapply', action='store_true', dest='reapply',
4223 help='Reset the branch and reapply the issue.\n'
4224 'CAUTION: This will undo any local changes in this '
4225 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004226
4227 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004228 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004229 parser.add_option_group(group)
4230
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004231 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004232 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004233 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004234 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004235 auth_config = auth.extract_auth_config_from_options(options)
4236
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004237
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004238 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004239 if options.newbranch:
4240 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004241 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004242 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004243
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004244 cl = Changelist(auth_config=auth_config,
4245 codereview=options.forced_codereview)
4246 if not cl.GetIssue():
4247 parser.error('current branch must have an associated issue')
4248
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004249 upstream = cl.GetUpstreamBranch()
4250 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004251 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004252
4253 RunGit(['reset', '--hard', upstream])
4254 if options.pull:
4255 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004256
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004257 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4258 options.directory)
4259
4260 if len(args) != 1 or not args[0]:
4261 parser.error('Must specify issue number or url')
4262
4263 # We don't want uncommitted changes mixed up with the patch.
4264 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004265 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004267 if options.newbranch:
4268 if options.force:
4269 RunGit(['branch', '-D', options.newbranch],
4270 stderr=subprocess2.PIPE, error_ok=True)
4271 RunGit(['new-branch', options.newbranch])
4272
4273 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4274
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004275 if cl.IsGerrit():
4276 if options.reject:
4277 parser.error('--reject is not supported with Gerrit codereview.')
4278 if options.nocommit:
4279 parser.error('--nocommit is not supported with Gerrit codereview.')
4280 if options.directory:
4281 parser.error('--directory is not supported with Gerrit codereview.')
4282
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004283 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004284 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004285
4286
4287def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004288 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004289 # Provide a wrapper for git svn rebase to help avoid accidental
4290 # git svn dcommit.
4291 # It's the only command that doesn't use parser at all since we just defer
4292 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004293
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004294 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004295
4296
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004297def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004298 """Fetches the tree status and returns either 'open', 'closed',
4299 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004300 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301 if url:
4302 status = urllib2.urlopen(url).read().lower()
4303 if status.find('closed') != -1 or status == '0':
4304 return 'closed'
4305 elif status.find('open') != -1 or status == '1':
4306 return 'open'
4307 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004308 return 'unset'
4309
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004311def GetTreeStatusReason():
4312 """Fetches the tree status from a json url and returns the message
4313 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004314 url = settings.GetTreeStatusUrl()
4315 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004316 connection = urllib2.urlopen(json_url)
4317 status = json.loads(connection.read())
4318 connection.close()
4319 return status['message']
4320
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004321
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004322def GetBuilderMaster(bot_list):
4323 """For a given builder, fetch the master from AE if available."""
4324 map_url = 'https://builders-map.appspot.com/'
4325 try:
4326 master_map = json.load(urllib2.urlopen(map_url))
4327 except urllib2.URLError as e:
4328 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4329 (map_url, e))
4330 except ValueError as e:
4331 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4332 if not master_map:
4333 return None, 'Failed to build master map.'
4334
4335 result_master = ''
4336 for bot in bot_list:
4337 builder = bot.split(':', 1)[0]
4338 master_list = master_map.get(builder, [])
4339 if not master_list:
4340 return None, ('No matching master for builder %s.' % builder)
4341 elif len(master_list) > 1:
4342 return None, ('The builder name %s exists in multiple masters %s.' %
4343 (builder, master_list))
4344 else:
4345 cur_master = master_list[0]
4346 if not result_master:
4347 result_master = cur_master
4348 elif result_master != cur_master:
4349 return None, 'The builders do not belong to the same master.'
4350 return result_master, None
4351
4352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004353def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004354 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004355 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004356 status = GetTreeStatus()
4357 if 'unset' == status:
4358 print 'You must configure your tree status URL by running "git cl config".'
4359 return 2
4360
4361 print "The tree is %s" % status
4362 print
4363 print GetTreeStatusReason()
4364 if status != 'open':
4365 return 1
4366 return 0
4367
4368
maruel@chromium.org15192402012-09-06 12:38:29 +00004369def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004370 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004371 group = optparse.OptionGroup(parser, "Try job options")
4372 group.add_option(
4373 "-b", "--bot", action="append",
4374 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4375 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004376 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004377 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004378 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004379 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004380 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004381 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004382 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004383 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004384 "-r", "--revision",
4385 help="Revision to use for the try job; default: the "
4386 "revision will be determined by the try server; see "
4387 "its waterfall for more info")
4388 group.add_option(
4389 "-c", "--clobber", action="store_true", default=False,
4390 help="Force a clobber before building; e.g. don't do an "
4391 "incremental build")
4392 group.add_option(
4393 "--project",
4394 help="Override which project to use. Projects are defined "
4395 "server-side to define what default bot set to use")
4396 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004397 "-p", "--property", dest="properties", action="append", default=[],
4398 help="Specify generic properties in the form -p key1=value1 -p "
4399 "key2=value2 etc (buildbucket only). The value will be treated as "
4400 "json if decodable, or as string otherwise.")
4401 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004402 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004403 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004404 "--use-rietveld", action="store_true", default=False,
4405 help="Use Rietveld to trigger try jobs.")
4406 group.add_option(
4407 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4408 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004409 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004410 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004411 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004412 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004413
machenbach@chromium.org45453142015-09-15 08:45:22 +00004414 if options.use_rietveld and options.properties:
4415 parser.error('Properties can only be specified with buildbucket')
4416
4417 # Make sure that all properties are prop=value pairs.
4418 bad_params = [x for x in options.properties if '=' not in x]
4419 if bad_params:
4420 parser.error('Got properties with missing "=": %s' % bad_params)
4421
maruel@chromium.org15192402012-09-06 12:38:29 +00004422 if args:
4423 parser.error('Unknown arguments: %s' % args)
4424
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004425 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004426 if not cl.GetIssue():
4427 parser.error('Need to upload first')
4428
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004429 if cl.IsGerrit():
4430 parser.error(
4431 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4432 'If your project has Commit Queue, dry run is a workaround:\n'
4433 ' git cl set-commit --dry-run')
4434 # Code below assumes Rietveld issue.
4435 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4436
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004437 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004438 if props.get('closed'):
4439 parser.error('Cannot send tryjobs for a closed CL')
4440
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004441 if props.get('private'):
4442 parser.error('Cannot use trybots with private issue')
4443
maruel@chromium.org15192402012-09-06 12:38:29 +00004444 if not options.name:
4445 options.name = cl.GetBranch()
4446
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004447 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004448 options.master, err_msg = GetBuilderMaster(options.bot)
4449 if err_msg:
4450 parser.error('Tryserver master cannot be found because: %s\n'
4451 'Please manually specify the tryserver master'
4452 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004453
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004454 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004455 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004456 if not options.bot:
4457 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004458
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004459 # Get try masters from PRESUBMIT.py files.
4460 masters = presubmit_support.DoGetTryMasters(
4461 change,
4462 change.LocalPaths(),
4463 settings.GetRoot(),
4464 None,
4465 None,
4466 options.verbose,
4467 sys.stdout)
4468 if masters:
4469 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004470
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004471 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4472 options.bot = presubmit_support.DoGetTrySlaves(
4473 change,
4474 change.LocalPaths(),
4475 settings.GetRoot(),
4476 None,
4477 None,
4478 options.verbose,
4479 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004480
4481 if not options.bot:
4482 # Get try masters from cq.cfg if any.
4483 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4484 # location.
4485 cq_cfg = os.path.join(change.RepositoryRoot(),
4486 'infra', 'config', 'cq.cfg')
4487 if os.path.exists(cq_cfg):
4488 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004489 cq_masters = commit_queue.get_master_builder_map(
4490 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004491 for master, builders in cq_masters.iteritems():
4492 for builder in builders:
4493 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004494 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004495 if masters:
tandriib93dd2b2016-06-07 08:03:08 -07004496 print('Loaded default bots from CQ config (%s)' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004497 return masters
tandriib93dd2b2016-06-07 08:03:08 -07004498 else:
4499 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004500
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004501 if not options.bot:
4502 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004503
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004504 builders_and_tests = {}
4505 # TODO(machenbach): The old style command-line options don't support
4506 # multiple try masters yet.
4507 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4508 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4509
4510 for bot in old_style:
4511 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004512 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004513 elif ',' in bot:
4514 parser.error('Specify one bot per --bot flag')
4515 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004516 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004517
4518 for bot, tests in new_style:
4519 builders_and_tests.setdefault(bot, []).extend(tests)
4520
4521 # Return a master map with one master to be backwards compatible. The
4522 # master name defaults to an empty string, which will cause the master
4523 # not to be set on rietveld (deprecated).
4524 return {options.master: builders_and_tests}
4525
4526 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004527
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004528 for builders in masters.itervalues():
4529 if any('triggered' in b for b in builders):
4530 print >> sys.stderr, (
4531 'ERROR You are trying to send a job to a triggered bot. This type of'
4532 ' bot requires an\ninitial job from a parent (usually a builder). '
4533 'Instead send your job to the parent.\n'
4534 'Bot list: %s' % builders)
4535 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004536
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004537 patchset = cl.GetMostRecentPatchset()
4538 if patchset and patchset != cl.GetPatchset():
4539 print(
4540 '\nWARNING Mismatch between local config and server. Did a previous '
4541 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4542 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004543 if options.luci:
4544 trigger_luci_job(cl, masters, options)
4545 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004546 try:
4547 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4548 except BuildbucketResponseException as ex:
4549 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004550 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004551 except Exception as e:
4552 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4553 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4554 e, stacktrace)
4555 return 1
4556 else:
4557 try:
4558 cl.RpcServer().trigger_distributed_try_jobs(
4559 cl.GetIssue(), patchset, options.name, options.clobber,
4560 options.revision, masters)
4561 except urllib2.HTTPError as e:
4562 if e.code == 404:
4563 print('404 from rietveld; '
4564 'did you mean to use "git try" instead of "git cl try"?')
4565 return 1
4566 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004567
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004568 for (master, builders) in sorted(masters.iteritems()):
4569 if master:
4570 print 'Master: %s' % master
4571 length = max(len(builder) for builder in builders)
4572 for builder in sorted(builders):
4573 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004574 return 0
4575
4576
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004577def CMDtry_results(parser, args):
4578 group = optparse.OptionGroup(parser, "Try job results options")
4579 group.add_option(
4580 "-p", "--patchset", type=int, help="patchset number if not current.")
4581 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004582 "--print-master", action='store_true', help="print master name as well.")
4583 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004584 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004585 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004586 group.add_option(
4587 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4588 help="Host of buildbucket. The default host is %default.")
4589 parser.add_option_group(group)
4590 auth.add_auth_options(parser)
4591 options, args = parser.parse_args(args)
4592 if args:
4593 parser.error('Unrecognized args: %s' % ' '.join(args))
4594
4595 auth_config = auth.extract_auth_config_from_options(options)
4596 cl = Changelist(auth_config=auth_config)
4597 if not cl.GetIssue():
4598 parser.error('Need to upload first')
4599
4600 if not options.patchset:
4601 options.patchset = cl.GetMostRecentPatchset()
4602 if options.patchset and options.patchset != cl.GetPatchset():
4603 print(
4604 '\nWARNING Mismatch between local config and server. Did a previous '
4605 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4606 'Continuing using\npatchset %s.\n' % options.patchset)
4607 try:
4608 jobs = fetch_try_jobs(auth_config, cl, options)
4609 except BuildbucketResponseException as ex:
4610 print 'Buildbucket error: %s' % ex
4611 return 1
4612 except Exception as e:
4613 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4614 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4615 e, stacktrace)
4616 return 1
4617 print_tryjobs(options, jobs)
4618 return 0
4619
4620
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004621@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004622def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004623 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004624 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004625 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004626 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004628 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004629 if args:
4630 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004631 branch = cl.GetBranch()
4632 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004633 cl = Changelist()
4634 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004635
4636 # Clear configured merge-base, if there is one.
4637 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004638 else:
4639 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004640 return 0
4641
4642
thestig@chromium.org00858c82013-12-02 23:08:03 +00004643def CMDweb(parser, args):
4644 """Opens the current CL in the web browser."""
4645 _, args = parser.parse_args(args)
4646 if args:
4647 parser.error('Unrecognized args: %s' % ' '.join(args))
4648
4649 issue_url = Changelist().GetIssueURL()
4650 if not issue_url:
4651 print >> sys.stderr, 'ERROR No issue to open'
4652 return 1
4653
4654 webbrowser.open(issue_url)
4655 return 0
4656
4657
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004658def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004659 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004660 parser.add_option('-d', '--dry-run', action='store_true',
4661 help='trigger in dry run mode')
4662 parser.add_option('-c', '--clear', action='store_true',
4663 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004664 auth.add_auth_options(parser)
4665 options, args = parser.parse_args(args)
4666 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004667 if args:
4668 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004669 if options.dry_run and options.clear:
4670 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4671
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004672 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004673 if options.clear:
4674 state = _CQState.CLEAR
4675 elif options.dry_run:
4676 state = _CQState.DRY_RUN
4677 else:
4678 state = _CQState.COMMIT
4679 if not cl.GetIssue():
4680 parser.error('Must upload the issue first')
4681 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004682 return 0
4683
4684
groby@chromium.org411034a2013-02-26 15:12:01 +00004685def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004686 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004687 auth.add_auth_options(parser)
4688 options, args = parser.parse_args(args)
4689 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004690 if args:
4691 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004692 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004693 # Ensure there actually is an issue to close.
4694 cl.GetDescription()
4695 cl.CloseIssue()
4696 return 0
4697
4698
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004699def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004700 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004701 auth.add_auth_options(parser)
4702 options, args = parser.parse_args(args)
4703 auth_config = auth.extract_auth_config_from_options(options)
4704 if args:
4705 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004706
4707 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004708 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004709 # Staged changes would be committed along with the patch from last
4710 # upload, hence counted toward the "last upload" side in the final
4711 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004712 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004713 return 1
4714
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004715 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004716 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004717 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004718 if not issue:
4719 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004720 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004721 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004722
4723 # Create a new branch based on the merge-base
4724 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004725 # Clear cached branch in cl object, to avoid overwriting original CL branch
4726 # properties.
4727 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004728 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004729 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004730 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004731 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004732 return rtn
4733
wychen@chromium.org06928532015-02-03 02:11:29 +00004734 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004735 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004736 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004737 finally:
4738 RunGit(['checkout', '-q', branch])
4739 RunGit(['branch', '-D', TMP_BRANCH])
4740
4741 return 0
4742
4743
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004744def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004745 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004746 parser.add_option(
4747 '--no-color',
4748 action='store_true',
4749 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004750 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004751 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004752 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004753
4754 author = RunGit(['config', 'user.email']).strip() or None
4755
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004756 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004757
4758 if args:
4759 if len(args) > 1:
4760 parser.error('Unknown args')
4761 base_branch = args[0]
4762 else:
4763 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004764 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004765
4766 change = cl.GetChange(base_branch, None)
4767 return owners_finder.OwnersFinder(
4768 [f.LocalPath() for f in
4769 cl.GetChange(base_branch, None).AffectedFiles()],
4770 change.RepositoryRoot(), author,
4771 fopen=file, os_path=os.path, glob=glob.glob,
4772 disable_color=options.no_color).run()
4773
4774
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004775def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004776 """Generates a diff command."""
4777 # Generate diff for the current branch's changes.
4778 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4779 upstream_commit, '--' ]
4780
4781 if args:
4782 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004783 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004784 diff_cmd.append(arg)
4785 else:
4786 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004787
4788 return diff_cmd
4789
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004790def MatchingFileType(file_name, extensions):
4791 """Returns true if the file name ends with one of the given extensions."""
4792 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004793
enne@chromium.org555cfe42014-01-29 18:21:39 +00004794@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004795def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004796 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004797 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004798 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004799 parser.add_option('--full', action='store_true',
4800 help='Reformat the full content of all touched files')
4801 parser.add_option('--dry-run', action='store_true',
4802 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004803 parser.add_option('--python', action='store_true',
4804 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004805 parser.add_option('--diff', action='store_true',
4806 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004807 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004808
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004809 # git diff generates paths against the root of the repository. Change
4810 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004811 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004812 if rel_base_path:
4813 os.chdir(rel_base_path)
4814
digit@chromium.org29e47272013-05-17 17:01:46 +00004815 # Grab the merge-base commit, i.e. the upstream commit of the current
4816 # branch when it was created or the last time it was rebased. This is
4817 # to cover the case where the user may have called "git fetch origin",
4818 # moving the origin branch to a newer commit, but hasn't rebased yet.
4819 upstream_commit = None
4820 cl = Changelist()
4821 upstream_branch = cl.GetUpstreamBranch()
4822 if upstream_branch:
4823 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4824 upstream_commit = upstream_commit.strip()
4825
4826 if not upstream_commit:
4827 DieWithError('Could not find base commit for this branch. '
4828 'Are you in detached state?')
4829
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004830 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4831 diff_output = RunGit(changed_files_cmd)
4832 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004833 # Filter out files deleted by this CL
4834 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004835
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004836 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4837 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4838 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004839 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004840
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004841 top_dir = os.path.normpath(
4842 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4843
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004844 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4845 # formatted. This is used to block during the presubmit.
4846 return_value = 0
4847
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004848 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004849 # Locate the clang-format binary in the checkout
4850 try:
4851 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4852 except clang_format.NotFoundError, e:
4853 DieWithError(e)
4854
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004855 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004856 cmd = [clang_format_tool]
4857 if not opts.dry_run and not opts.diff:
4858 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004859 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004860 if opts.diff:
4861 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004862 else:
4863 env = os.environ.copy()
4864 env['PATH'] = str(os.path.dirname(clang_format_tool))
4865 try:
4866 script = clang_format.FindClangFormatScriptInChromiumTree(
4867 'clang-format-diff.py')
4868 except clang_format.NotFoundError, e:
4869 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004870
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004871 cmd = [sys.executable, script, '-p0']
4872 if not opts.dry_run and not opts.diff:
4873 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004874
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004875 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4876 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004877
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004878 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4879 if opts.diff:
4880 sys.stdout.write(stdout)
4881 if opts.dry_run and len(stdout) > 0:
4882 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004883
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004884 # Similar code to above, but using yapf on .py files rather than clang-format
4885 # on C/C++ files
4886 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004887 yapf_tool = gclient_utils.FindExecutable('yapf')
4888 if yapf_tool is None:
4889 DieWithError('yapf not found in PATH')
4890
4891 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004892 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004893 cmd = [yapf_tool]
4894 if not opts.dry_run and not opts.diff:
4895 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004896 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004897 if opts.diff:
4898 sys.stdout.write(stdout)
4899 else:
4900 # TODO(sbc): yapf --lines mode still has some issues.
4901 # https://github.com/google/yapf/issues/154
4902 DieWithError('--python currently only works with --full')
4903
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004904 # Dart's formatter does not have the nice property of only operating on
4905 # modified chunks, so hard code full.
4906 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004907 try:
4908 command = [dart_format.FindDartFmtToolInChromiumTree()]
4909 if not opts.dry_run and not opts.diff:
4910 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004911 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004912
ppi@chromium.org6593d932016-03-03 15:41:15 +00004913 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004914 if opts.dry_run and stdout:
4915 return_value = 2
4916 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004917 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4918 'found in this checkout. Files in other languages are still ' +
4919 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004920
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004921 # Format GN build files. Always run on full build files for canonical form.
4922 if gn_diff_files:
4923 cmd = ['gn', 'format']
4924 if not opts.dry_run and not opts.diff:
4925 cmd.append('--in-place')
4926 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004927 stdout = RunCommand(cmd + [gn_diff_file],
4928 shell=sys.platform == 'win32',
4929 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004930 if opts.diff:
4931 sys.stdout.write(stdout)
4932
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004933 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004934
4935
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004936@subcommand.usage('<codereview url or issue id>')
4937def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004938 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004939 _, args = parser.parse_args(args)
4940
4941 if len(args) != 1:
4942 parser.print_help()
4943 return 1
4944
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004945 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004946 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004947 parser.print_help()
4948 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004949 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004950
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004951 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004952 output = RunGit(['config', '--local', '--get-regexp',
4953 r'branch\..*\.%s' % issueprefix],
4954 error_ok=True)
4955 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004956 if issue == target_issue:
4957 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004958
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004959 branches = []
4960 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004961 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004962 if len(branches) == 0:
4963 print 'No branch found for issue %s.' % target_issue
4964 return 1
4965 if len(branches) == 1:
4966 RunGit(['checkout', branches[0]])
4967 else:
4968 print 'Multiple branches match issue %s:' % target_issue
4969 for i in range(len(branches)):
4970 print '%d: %s' % (i, branches[i])
4971 which = raw_input('Choose by index: ')
4972 try:
4973 RunGit(['checkout', branches[int(which)]])
4974 except (IndexError, ValueError):
4975 print 'Invalid selection, not checking out any branch.'
4976 return 1
4977
4978 return 0
4979
4980
maruel@chromium.org29404b52014-09-08 22:58:00 +00004981def CMDlol(parser, args):
4982 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004983 print zlib.decompress(base64.b64decode(
4984 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4985 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4986 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4987 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004988 return 0
4989
4990
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004991class OptionParser(optparse.OptionParser):
4992 """Creates the option parse and add --verbose support."""
4993 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004994 optparse.OptionParser.__init__(
4995 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004996 self.add_option(
4997 '-v', '--verbose', action='count', default=0,
4998 help='Use 2 times for more debugging info')
4999
5000 def parse_args(self, args=None, values=None):
5001 options, args = optparse.OptionParser.parse_args(self, args, values)
5002 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5003 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5004 return options, args
5005
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005007def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005008 if sys.hexversion < 0x02060000:
5009 print >> sys.stderr, (
5010 '\nYour python version %s is unsupported, please upgrade.\n' %
5011 sys.version.split(' ', 1)[0])
5012 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005013
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005014 # Reload settings.
5015 global settings
5016 settings = Settings()
5017
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005018 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005019 dispatcher = subcommand.CommandDispatcher(__name__)
5020 try:
5021 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005022 except auth.AuthenticationError as e:
5023 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005024 except urllib2.HTTPError, e:
5025 if e.code != 500:
5026 raise
5027 DieWithError(
5028 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5029 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005030 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005031
5032
5033if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005034 # These affect sys.stdout so do it outside of main() to simplify mocks in
5035 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005036 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005037 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005038 try:
5039 sys.exit(main(sys.argv[1:]))
5040 except KeyboardInterrupt:
5041 sys.stderr.write('interrupted\n')
5042 sys.exit(1)