blob: b0c42f4aacd474ad3c9ee29bffc352bde5fe597f [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
tandrii@chromium.org04ea8462016-04-25 19:51:21 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
tandrii@chromium.org04ea8462016-04-25 19:51:21 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
bsep@chromium.org627d9002016-04-29 00:00:52 +000098def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000100 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000340 if 'presubmit' in builder.lower():
341 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000342 if tests:
343 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000344 if properties:
345 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000346 if options.clobber:
347 parameters['properties']['clobber'] = True
348 batch_req_body['builds'].append(
349 {
350 'bucket': bucket,
351 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000352 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000353 'tags': ['builder:%s' % builder,
354 'buildset:%s' % buildset,
355 'master:%s' % master,
356 'user_agent:git_cl_try']
357 }
358 )
359
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360 _buildbucket_retry(
361 'triggering tryjobs',
362 http,
363 buildbucket_put_url,
364 'PUT',
365 body=json.dumps(batch_req_body),
366 headers={'Content-Type': 'application/json'}
367 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000368 print_text.append('To see results here, run: git cl try-results')
369 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000371
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000372
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000373def fetch_try_jobs(auth_config, changelist, options):
374 """Fetches tryjobs from buildbucket.
375
376 Returns a map from build id to build info as json dictionary.
377 """
378 rietveld_url = settings.GetDefaultServerUrl()
379 rietveld_host = urlparse.urlparse(rietveld_url).hostname
380 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
381 if authenticator.has_cached_credentials():
382 http = authenticator.authorize(httplib2.Http())
383 else:
384 print ('Warning: Some results might be missing because %s' %
385 # Get the message on how to login.
386 auth.LoginRequiredError(rietveld_host).message)
387 http = httplib2.Http()
388
389 http.force_exception_to_status_code = True
390
391 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
392 hostname=rietveld_host,
393 issue=changelist.GetIssue(),
394 patch=options.patchset)
395 params = {'tag': 'buildset:%s' % buildset}
396
397 builds = {}
398 while True:
399 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
400 hostname=options.buildbucket_host,
401 params=urllib.urlencode(params))
402 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
403 for build in content.get('builds', []):
404 builds[build['id']] = build
405 if 'next_cursor' in content:
406 params['start_cursor'] = content['next_cursor']
407 else:
408 break
409 return builds
410
411
412def print_tryjobs(options, builds):
413 """Prints nicely result of fetch_try_jobs."""
414 if not builds:
415 print 'No tryjobs scheduled'
416 return
417
418 # Make a copy, because we'll be modifying builds dictionary.
419 builds = builds.copy()
420 builder_names_cache = {}
421
422 def get_builder(b):
423 try:
424 return builder_names_cache[b['id']]
425 except KeyError:
426 try:
427 parameters = json.loads(b['parameters_json'])
428 name = parameters['builder_name']
429 except (ValueError, KeyError) as error:
430 print 'WARNING: failed to get builder name for build %s: %s' % (
431 b['id'], error)
432 name = None
433 builder_names_cache[b['id']] = name
434 return name
435
436 def get_bucket(b):
437 bucket = b['bucket']
438 if bucket.startswith('master.'):
439 return bucket[len('master.'):]
440 return bucket
441
442 if options.print_master:
443 name_fmt = '%%-%ds %%-%ds' % (
444 max(len(str(get_bucket(b))) for b in builds.itervalues()),
445 max(len(str(get_builder(b))) for b in builds.itervalues()))
446 def get_name(b):
447 return name_fmt % (get_bucket(b), get_builder(b))
448 else:
449 name_fmt = '%%-%ds' % (
450 max(len(str(get_builder(b))) for b in builds.itervalues()))
451 def get_name(b):
452 return name_fmt % get_builder(b)
453
454 def sort_key(b):
455 return b['status'], b.get('result'), get_name(b), b.get('url')
456
457 def pop(title, f, color=None, **kwargs):
458 """Pop matching builds from `builds` dict and print them."""
459
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000460 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461 colorize = str
462 else:
463 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
464
465 result = []
466 for b in builds.values():
467 if all(b.get(k) == v for k, v in kwargs.iteritems()):
468 builds.pop(b['id'])
469 result.append(b)
470 if result:
471 print colorize(title)
472 for b in sorted(result, key=sort_key):
473 print ' ', colorize('\t'.join(map(str, f(b))))
474
475 total = len(builds)
476 pop(status='COMPLETED', result='SUCCESS',
477 title='Successes:', color=Fore.GREEN,
478 f=lambda b: (get_name(b), b.get('url')))
479 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
480 title='Infra Failures:', color=Fore.MAGENTA,
481 f=lambda b: (get_name(b), b.get('url')))
482 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
483 title='Failures:', color=Fore.RED,
484 f=lambda b: (get_name(b), b.get('url')))
485 pop(status='COMPLETED', result='CANCELED',
486 title='Canceled:', color=Fore.MAGENTA,
487 f=lambda b: (get_name(b),))
488 pop(status='COMPLETED', result='FAILURE',
489 failure_reason='INVALID_BUILD_DEFINITION',
490 title='Wrong master/builder name:', color=Fore.MAGENTA,
491 f=lambda b: (get_name(b),))
492 pop(status='COMPLETED', result='FAILURE',
493 title='Other failures:',
494 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
495 pop(status='COMPLETED',
496 title='Other finished:',
497 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
498 pop(status='STARTED',
499 title='Started:', color=Fore.YELLOW,
500 f=lambda b: (get_name(b), b.get('url')))
501 pop(status='SCHEDULED',
502 title='Scheduled:',
503 f=lambda b: (get_name(b), 'id=%s' % b['id']))
504 # The last section is just in case buildbucket API changes OR there is a bug.
505 pop(title='Other:',
506 f=lambda b: (get_name(b), 'id=%s' % b['id']))
507 assert len(builds) == 0
508 print 'Total: %d tryjobs' % total
509
510
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000511def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
512 """Return the corresponding git ref if |base_url| together with |glob_spec|
513 matches the full |url|.
514
515 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
516 """
517 fetch_suburl, as_ref = glob_spec.split(':')
518 if allow_wildcards:
519 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
520 if glob_match:
521 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
522 # "branches/{472,597,648}/src:refs/remotes/svn/*".
523 branch_re = re.escape(base_url)
524 if glob_match.group(1):
525 branch_re += '/' + re.escape(glob_match.group(1))
526 wildcard = glob_match.group(2)
527 if wildcard == '*':
528 branch_re += '([^/]*)'
529 else:
530 # Escape and replace surrounding braces with parentheses and commas
531 # with pipe symbols.
532 wildcard = re.escape(wildcard)
533 wildcard = re.sub('^\\\\{', '(', wildcard)
534 wildcard = re.sub('\\\\,', '|', wildcard)
535 wildcard = re.sub('\\\\}$', ')', wildcard)
536 branch_re += wildcard
537 if glob_match.group(3):
538 branch_re += re.escape(glob_match.group(3))
539 match = re.match(branch_re, url)
540 if match:
541 return re.sub('\*$', match.group(1), as_ref)
542
543 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
544 if fetch_suburl:
545 full_url = base_url + '/' + fetch_suburl
546 else:
547 full_url = base_url
548 if full_url == url:
549 return as_ref
550 return None
551
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000552
iannucci@chromium.org79540052012-10-19 23:15:26 +0000553def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000554 """Prints statistics about the change to the user."""
555 # --no-ext-diff is broken in some versions of Git, so try to work around
556 # this by overriding the environment (but there is still a problem if the
557 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000558 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000559 if 'GIT_EXTERNAL_DIFF' in env:
560 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000561
562 if find_copies:
563 similarity_options = ['--find-copies-harder', '-l100000',
564 '-C%s' % similarity]
565 else:
566 similarity_options = ['-M%s' % similarity]
567
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000568 try:
569 stdout = sys.stdout.fileno()
570 except AttributeError:
571 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000572 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000573 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000574 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000575 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000576
577
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000578class BuildbucketResponseException(Exception):
579 pass
580
581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000582class Settings(object):
583 def __init__(self):
584 self.default_server = None
585 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000586 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 self.is_git_svn = None
588 self.svn_branch = None
589 self.tree_status_url = None
590 self.viewvc_url = None
591 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000592 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000593 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000594 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000595 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000596 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000597 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000598 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599
600 def LazyUpdateIfNeeded(self):
601 """Updates the settings from a codereview.settings file, if available."""
602 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 # The only value that actually changes the behavior is
604 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000605 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000606 error_ok=True
607 ).strip().lower()
608
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000609 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000610 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 LoadCodereviewSettingsFromFile(cr_settings_file)
612 self.updated = True
613
614 def GetDefaultServerUrl(self, error_ok=False):
615 if not self.default_server:
616 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000617 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000618 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 if error_ok:
620 return self.default_server
621 if not self.default_server:
622 error_message = ('Could not find settings file. You must configure '
623 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000624 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000625 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 return self.default_server
627
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000628 @staticmethod
629 def GetRelativeRoot():
630 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000633 if self.root is None:
634 self.root = os.path.abspath(self.GetRelativeRoot())
635 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000636
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 def GetGitMirror(self, remote='origin'):
638 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000639 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000640 if not os.path.isdir(local_url):
641 return None
642 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
643 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
644 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
645 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
646 if mirror.exists():
647 return mirror
648 return None
649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 def GetIsGitSvn(self):
651 """Return true if this repo looks like it's using git-svn."""
652 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000653 if self.GetPendingRefPrefix():
654 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
655 self.is_git_svn = False
656 else:
657 # If you have any "svn-remote.*" config keys, we think you're using svn.
658 self.is_git_svn = RunGitWithCode(
659 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 return self.is_git_svn
661
662 def GetSVNBranch(self):
663 if self.svn_branch is None:
664 if not self.GetIsGitSvn():
665 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
666
667 # Try to figure out which remote branch we're based on.
668 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000669 # 1) iterate through our branch history and find the svn URL.
670 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671
672 # regexp matching the git-svn line that contains the URL.
673 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
674
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000675 # We don't want to go through all of history, so read a line from the
676 # pipe at a time.
677 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000678 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000679 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
680 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000681 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000682 for line in proc.stdout:
683 match = git_svn_re.match(line)
684 if match:
685 url = match.group(1)
686 proc.stdout.close() # Cut pipe.
687 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000688
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000689 if url:
690 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
691 remotes = RunGit(['config', '--get-regexp',
692 r'^svn-remote\..*\.url']).splitlines()
693 for remote in remotes:
694 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000696 remote = match.group(1)
697 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000698 rewrite_root = RunGit(
699 ['config', 'svn-remote.%s.rewriteRoot' % remote],
700 error_ok=True).strip()
701 if rewrite_root:
702 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000703 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000704 ['config', 'svn-remote.%s.fetch' % remote],
705 error_ok=True).strip()
706 if fetch_spec:
707 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
708 if self.svn_branch:
709 break
710 branch_spec = RunGit(
711 ['config', 'svn-remote.%s.branches' % remote],
712 error_ok=True).strip()
713 if branch_spec:
714 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
715 if self.svn_branch:
716 break
717 tag_spec = RunGit(
718 ['config', 'svn-remote.%s.tags' % remote],
719 error_ok=True).strip()
720 if tag_spec:
721 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
722 if self.svn_branch:
723 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724
725 if not self.svn_branch:
726 DieWithError('Can\'t guess svn branch -- try specifying it on the '
727 'command line')
728
729 return self.svn_branch
730
731 def GetTreeStatusUrl(self, error_ok=False):
732 if not self.tree_status_url:
733 error_message = ('You must configure your tree status URL by running '
734 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000735 self.tree_status_url = self._GetRietveldConfig(
736 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 return self.tree_status_url
738
739 def GetViewVCUrl(self):
740 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742 return self.viewvc_url
743
rmistry@google.com90752582014-01-14 21:04:50 +0000744 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000745 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000746
rmistry@google.com78948ed2015-07-08 23:09:57 +0000747 def GetIsSkipDependencyUpload(self, branch_name):
748 """Returns true if specified branch should skip dep uploads."""
749 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
750 error_ok=True)
751
rmistry@google.com5626a922015-02-26 14:03:30 +0000752 def GetRunPostUploadHook(self):
753 run_post_upload_hook = self._GetRietveldConfig(
754 'run-post-upload-hook', error_ok=True)
755 return run_post_upload_hook == "True"
756
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000757 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000759
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000760 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000761 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000762
ukai@chromium.orge8077812012-02-03 03:41:46 +0000763 def GetIsGerrit(self):
764 """Return true if this repo is assosiated with gerrit code review system."""
765 if self.is_gerrit is None:
766 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
767 return self.is_gerrit
768
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000769 def GetSquashGerritUploads(self):
770 """Return true if uploads to Gerrit should be squashed by default."""
771 if self.squash_gerrit_uploads is None:
772 self.squash_gerrit_uploads = (
773 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
774 error_ok=True).strip() == 'true')
775 return self.squash_gerrit_uploads
776
tandrii@chromium.org28253532016-04-14 13:46:56 +0000777 def GetGerritSkipEnsureAuthenticated(self):
778 """Return True if EnsureAuthenticated should not be done for Gerrit
779 uploads."""
780 if self.gerrit_skip_ensure_authenticated is None:
781 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000782 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000783 error_ok=True).strip() == 'true')
784 return self.gerrit_skip_ensure_authenticated
785
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000786 def GetGitEditor(self):
787 """Return the editor specified in the git config, or None if none is."""
788 if self.git_editor is None:
789 self.git_editor = self._GetConfig('core.editor', error_ok=True)
790 return self.git_editor or None
791
thestig@chromium.org44202a22014-03-11 19:22:18 +0000792 def GetLintRegex(self):
793 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
794 DEFAULT_LINT_REGEX)
795
796 def GetLintIgnoreRegex(self):
797 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
798 DEFAULT_LINT_IGNORE_REGEX)
799
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000800 def GetProject(self):
801 if not self.project:
802 self.project = self._GetRietveldConfig('project', error_ok=True)
803 return self.project
804
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000805 def GetForceHttpsCommitUrl(self):
806 if not self.force_https_commit_url:
807 self.force_https_commit_url = self._GetRietveldConfig(
808 'force-https-commit-url', error_ok=True)
809 return self.force_https_commit_url
810
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000811 def GetPendingRefPrefix(self):
812 if not self.pending_ref_prefix:
813 self.pending_ref_prefix = self._GetRietveldConfig(
814 'pending-ref-prefix', error_ok=True)
815 return self.pending_ref_prefix
816
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000817 def _GetRietveldConfig(self, param, **kwargs):
818 return self._GetConfig('rietveld.' + param, **kwargs)
819
rmistry@google.com78948ed2015-07-08 23:09:57 +0000820 def _GetBranchConfig(self, branch_name, param, **kwargs):
821 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
822
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823 def _GetConfig(self, param, **kwargs):
824 self.LazyUpdateIfNeeded()
825 return RunGit(['config', param], **kwargs).strip()
826
827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828def ShortBranchName(branch):
829 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000830 return branch.replace('refs/heads/', '', 1)
831
832
833def GetCurrentBranchRef():
834 """Returns branch ref (e.g., refs/heads/master) or None."""
835 return RunGit(['symbolic-ref', 'HEAD'],
836 stderr=subprocess2.VOID, error_ok=True).strip() or None
837
838
839def GetCurrentBranch():
840 """Returns current branch or None.
841
842 For refs/heads/* branches, returns just last part. For others, full ref.
843 """
844 branchref = GetCurrentBranchRef()
845 if branchref:
846 return ShortBranchName(branchref)
847 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
849
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000850class _CQState(object):
851 """Enum for states of CL with respect to Commit Queue."""
852 NONE = 'none'
853 DRY_RUN = 'dry_run'
854 COMMIT = 'commit'
855
856 ALL_STATES = [NONE, DRY_RUN, COMMIT]
857
858
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000859class _ParsedIssueNumberArgument(object):
860 def __init__(self, issue=None, patchset=None, hostname=None):
861 self.issue = issue
862 self.patchset = patchset
863 self.hostname = hostname
864
865 @property
866 def valid(self):
867 return self.issue is not None
868
869
870class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
871 def __init__(self, *args, **kwargs):
872 self.patch_url = kwargs.pop('patch_url', None)
873 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
874
875
876def ParseIssueNumberArgument(arg):
877 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
878 fail_result = _ParsedIssueNumberArgument()
879
880 if arg.isdigit():
881 return _ParsedIssueNumberArgument(issue=int(arg))
882 if not arg.startswith('http'):
883 return fail_result
884 url = gclient_utils.UpgradeToHttps(arg)
885 try:
886 parsed_url = urlparse.urlparse(url)
887 except ValueError:
888 return fail_result
889 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
890 tmp = cls.ParseIssueURL(parsed_url)
891 if tmp is not None:
892 return tmp
893 return fail_result
894
895
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000897 """Changelist works with one changelist in local branch.
898
899 Supports two codereview backends: Rietveld or Gerrit, selected at object
900 creation.
901
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +0000902 Notes:
903 * Not safe for concurrent multi-{thread,process} use.
904 * Caches values from current branch. Therefore, re-use after branch change
905 with care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000906 """
907
908 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
909 """Create a new ChangeList instance.
910
911 If issue is given, the codereview must be given too.
912
913 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
914 Otherwise, it's decided based on current configuration of the local branch,
915 with default being 'rietveld' for backwards compatibility.
916 See _load_codereview_impl for more details.
917
918 **kwargs will be passed directly to codereview implementation.
919 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000920 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000921 global settings
922 if not settings:
923 # Happens when git_cl.py is used as a utility library.
924 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000925
926 if issue:
927 assert codereview, 'codereview must be known, if issue is known'
928
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 self.branchref = branchref
930 if self.branchref:
931 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:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001258 current_issue = self.GetIssue()
1259 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001260 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001261 self.issue = None
1262 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001263
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001264 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001265 if not self.GitSanityChecks(upstream_branch):
1266 DieWithError('\nGit sanity check failure')
1267
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001268 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001269 if not root:
1270 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001271 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001272
1273 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001274 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001275 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001276 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001277 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001278 except subprocess2.CalledProcessError:
1279 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001280 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001281 'This branch probably doesn\'t exist anymore. To reset the\n'
1282 'tracking branch, please run\n'
1283 ' git branch --set-upstream %s trunk\n'
1284 'replacing trunk with origin/master or the relevant branch') %
1285 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001286
maruel@chromium.org52424302012-08-29 15:14:30 +00001287 issue = self.GetIssue()
1288 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001289 if issue:
1290 description = self.GetDescription()
1291 else:
1292 # If the change was never uploaded, use the log messages of all commits
1293 # up to the branch point, as git cl upload will prefill the description
1294 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001295 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1296 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001297
1298 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001299 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001300 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001301 name,
1302 description,
1303 absroot,
1304 files,
1305 issue,
1306 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001307 author,
1308 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001309
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310 def UpdateDescription(self, description):
1311 self.description = description
1312 return self._codereview_impl.UpdateDescriptionRemote(description)
1313
1314 def RunHook(self, committing, may_prompt, verbose, change):
1315 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1316 try:
1317 return presubmit_support.DoPresubmitChecks(change, committing,
1318 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1319 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001320 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1321 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001322 except presubmit_support.PresubmitFailure, e:
1323 DieWithError(
1324 ('%s\nMaybe your depot_tools is out of date?\n'
1325 'If all fails, contact maruel@') % e)
1326
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001327 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1328 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001329 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1330 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001331 else:
1332 # Assume url.
1333 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1334 urlparse.urlparse(issue_arg))
1335 if not parsed_issue_arg or not parsed_issue_arg.valid:
1336 DieWithError('Failed to parse issue argument "%s". '
1337 'Must be an issue number or a valid URL.' % issue_arg)
1338 return self._codereview_impl.CMDPatchWithParsedIssue(
1339 parsed_issue_arg, reject, nocommit, directory)
1340
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001341 def CMDUpload(self, options, git_diff_args, orig_args):
1342 """Uploads a change to codereview."""
1343 if git_diff_args:
1344 # TODO(ukai): is it ok for gerrit case?
1345 base_branch = git_diff_args[0]
1346 else:
1347 if self.GetBranch() is None:
1348 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1349
1350 # Default to diffing against common ancestor of upstream branch
1351 base_branch = self.GetCommonAncestorWithUpstream()
1352 git_diff_args = [base_branch, 'HEAD']
1353
1354 # Make sure authenticated to codereview before running potentially expensive
1355 # hooks. It is a fast, best efforts check. Codereview still can reject the
1356 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001357 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001358
1359 # Apply watchlists on upload.
1360 change = self.GetChange(base_branch, None)
1361 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1362 files = [f.LocalPath() for f in change.AffectedFiles()]
1363 if not options.bypass_watchlists:
1364 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1365
1366 if not options.bypass_hooks:
1367 if options.reviewers or options.tbr_owners:
1368 # Set the reviewer list now so that presubmit checks can access it.
1369 change_description = ChangeDescription(change.FullDescriptionText())
1370 change_description.update_reviewers(options.reviewers,
1371 options.tbr_owners,
1372 change)
1373 change.SetDescriptionText(change_description.description)
1374 hook_results = self.RunHook(committing=False,
1375 may_prompt=not options.force,
1376 verbose=options.verbose,
1377 change=change)
1378 if not hook_results.should_continue():
1379 return 1
1380 if not options.reviewers and hook_results.reviewers:
1381 options.reviewers = hook_results.reviewers.split(',')
1382
1383 if self.GetIssue():
1384 latest_patchset = self.GetMostRecentPatchset()
1385 local_patchset = self.GetPatchset()
1386 if (latest_patchset and local_patchset and
1387 local_patchset != latest_patchset):
1388 print ('The last upload made from this repository was patchset #%d but '
1389 'the most recent patchset on the server is #%d.'
1390 % (local_patchset, latest_patchset))
1391 print ('Uploading will still work, but if you\'ve uploaded to this '
1392 'issue from another machine or branch the patch you\'re '
1393 'uploading now might not include those changes.')
1394 ask_for_data('About to upload; enter to confirm.')
1395
1396 print_stats(options.similarity, options.find_copies, git_diff_args)
1397 ret = self.CMDUploadChange(options, git_diff_args, change)
1398 if not ret:
1399 git_set_branch_value('last-upload-hash',
1400 RunGit(['rev-parse', 'HEAD']).strip())
1401 # Run post upload hooks, if specified.
1402 if settings.GetRunPostUploadHook():
1403 presubmit_support.DoPostUploadExecuter(
1404 change,
1405 self,
1406 settings.GetRoot(),
1407 options.verbose,
1408 sys.stdout)
1409
1410 # Upload all dependencies if specified.
1411 if options.dependencies:
1412 print
1413 print '--dependencies has been specified.'
1414 print 'All dependent local branches will be re-uploaded.'
1415 print
1416 # Remove the dependencies flag from args so that we do not end up in a
1417 # loop.
1418 orig_args.remove('--dependencies')
1419 ret = upload_branch_deps(self, orig_args)
1420 return ret
1421
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001422 def SetCQState(self, new_state):
1423 """Update the CQ state for latest patchset.
1424
1425 Issue must have been already uploaded and known.
1426 """
1427 assert new_state in _CQState.ALL_STATES
1428 assert self.GetIssue()
1429 return self._codereview_impl.SetCQState(new_state)
1430
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001431 # Forward methods to codereview specific implementation.
1432
1433 def CloseIssue(self):
1434 return self._codereview_impl.CloseIssue()
1435
1436 def GetStatus(self):
1437 return self._codereview_impl.GetStatus()
1438
1439 def GetCodereviewServer(self):
1440 return self._codereview_impl.GetCodereviewServer()
1441
1442 def GetApprovingReviewers(self):
1443 return self._codereview_impl.GetApprovingReviewers()
1444
1445 def GetMostRecentPatchset(self):
1446 return self._codereview_impl.GetMostRecentPatchset()
1447
1448 def __getattr__(self, attr):
1449 # This is because lots of untested code accesses Rietveld-specific stuff
1450 # directly, and it's hard to fix for sure. So, just let it work, and fix
1451 # on a cases by case basis.
1452 return getattr(self._codereview_impl, attr)
1453
1454
1455class _ChangelistCodereviewBase(object):
1456 """Abstract base class encapsulating codereview specifics of a changelist."""
1457 def __init__(self, changelist):
1458 self._changelist = changelist # instance of Changelist
1459
1460 def __getattr__(self, attr):
1461 # Forward methods to changelist.
1462 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1463 # _RietveldChangelistImpl to avoid this hack?
1464 return getattr(self._changelist, attr)
1465
1466 def GetStatus(self):
1467 """Apply a rough heuristic to give a simple summary of an issue's review
1468 or CQ status, assuming adherence to a common workflow.
1469
1470 Returns None if no issue for this branch, or specific string keywords.
1471 """
1472 raise NotImplementedError()
1473
1474 def GetCodereviewServer(self):
1475 """Returns server URL without end slash, like "https://codereview.com"."""
1476 raise NotImplementedError()
1477
1478 def FetchDescription(self):
1479 """Fetches and returns description from the codereview server."""
1480 raise NotImplementedError()
1481
1482 def GetCodereviewServerSetting(self):
1483 """Returns git config setting for the codereview server."""
1484 raise NotImplementedError()
1485
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001486 @classmethod
1487 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001488 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001489
1490 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001491 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001492 """Returns name of git config setting which stores issue number for a given
1493 branch."""
1494 raise NotImplementedError()
1495
1496 def PatchsetSetting(self):
1497 """Returns name of git config setting which stores issue number."""
1498 raise NotImplementedError()
1499
1500 def GetRieveldObjForPresubmit(self):
1501 # This is an unfortunate Rietveld-embeddedness in presubmit.
1502 # For non-Rietveld codereviews, this probably should return a dummy object.
1503 raise NotImplementedError()
1504
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001505 def GetGerritObjForPresubmit(self):
1506 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1507 return None
1508
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001509 def UpdateDescriptionRemote(self, description):
1510 """Update the description on codereview site."""
1511 raise NotImplementedError()
1512
1513 def CloseIssue(self):
1514 """Closes the issue."""
1515 raise NotImplementedError()
1516
1517 def GetApprovingReviewers(self):
1518 """Returns a list of reviewers approving the change.
1519
1520 Note: not necessarily committers.
1521 """
1522 raise NotImplementedError()
1523
1524 def GetMostRecentPatchset(self):
1525 """Returns the most recent patchset number from the codereview site."""
1526 raise NotImplementedError()
1527
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001528 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1529 directory):
1530 """Fetches and applies the issue.
1531
1532 Arguments:
1533 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1534 reject: if True, reject the failed patch instead of switching to 3-way
1535 merge. Rietveld only.
1536 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1537 only.
1538 directory: switch to directory before applying the patch. Rietveld only.
1539 """
1540 raise NotImplementedError()
1541
1542 @staticmethod
1543 def ParseIssueURL(parsed_url):
1544 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1545 failed."""
1546 raise NotImplementedError()
1547
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001548 def EnsureAuthenticated(self, force):
1549 """Best effort check that user is authenticated with codereview server.
1550
1551 Arguments:
1552 force: whether to skip confirmation questions.
1553 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001554 raise NotImplementedError()
1555
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001556 def CMDUploadChange(self, options, args, change):
1557 """Uploads a change to codereview."""
1558 raise NotImplementedError()
1559
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001560 def SetCQState(self, new_state):
1561 """Update the CQ state for latest patchset.
1562
1563 Issue must have been already uploaded and known.
1564 """
1565 raise NotImplementedError()
1566
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567
1568class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1569 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1570 super(_RietveldChangelistImpl, self).__init__(changelist)
1571 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1572 settings.GetDefaultServerUrl()
1573
1574 self._rietveld_server = rietveld_server
1575 self._auth_config = auth_config
1576 self._props = None
1577 self._rpc_server = None
1578
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001579 def GetCodereviewServer(self):
1580 if not self._rietveld_server:
1581 # If we're on a branch then get the server potentially associated
1582 # with that branch.
1583 if self.GetIssue():
1584 rietveld_server_setting = self.GetCodereviewServerSetting()
1585 if rietveld_server_setting:
1586 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1587 ['config', rietveld_server_setting], error_ok=True).strip())
1588 if not self._rietveld_server:
1589 self._rietveld_server = settings.GetDefaultServerUrl()
1590 return self._rietveld_server
1591
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001592 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001593 """Best effort check that user is authenticated with Rietveld server."""
1594 if self._auth_config.use_oauth2:
1595 authenticator = auth.get_authenticator_for_host(
1596 self.GetCodereviewServer(), self._auth_config)
1597 if not authenticator.has_cached_credentials():
1598 raise auth.LoginRequiredError(self.GetCodereviewServer())
1599
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001600 def FetchDescription(self):
1601 issue = self.GetIssue()
1602 assert issue
1603 try:
1604 return self.RpcServer().get_description(issue).strip()
1605 except urllib2.HTTPError as e:
1606 if e.code == 404:
1607 DieWithError(
1608 ('\nWhile fetching the description for issue %d, received a '
1609 '404 (not found)\n'
1610 'error. It is likely that you deleted this '
1611 'issue on the server. If this is the\n'
1612 'case, please run\n\n'
1613 ' git cl issue 0\n\n'
1614 'to clear the association with the deleted issue. Then run '
1615 'this command again.') % issue)
1616 else:
1617 DieWithError(
1618 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1619 except urllib2.URLError as e:
1620 print >> sys.stderr, (
1621 'Warning: Failed to retrieve CL description due to network '
1622 'failure.')
1623 return ''
1624
1625 def GetMostRecentPatchset(self):
1626 return self.GetIssueProperties()['patchsets'][-1]
1627
1628 def GetPatchSetDiff(self, issue, patchset):
1629 return self.RpcServer().get(
1630 '/download/issue%s_%s.diff' % (issue, patchset))
1631
1632 def GetIssueProperties(self):
1633 if self._props is None:
1634 issue = self.GetIssue()
1635 if not issue:
1636 self._props = {}
1637 else:
1638 self._props = self.RpcServer().get_issue_properties(issue, True)
1639 return self._props
1640
1641 def GetApprovingReviewers(self):
1642 return get_approving_reviewers(self.GetIssueProperties())
1643
1644 def AddComment(self, message):
1645 return self.RpcServer().add_comment(self.GetIssue(), message)
1646
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001647 def GetStatus(self):
1648 """Apply a rough heuristic to give a simple summary of an issue's review
1649 or CQ status, assuming adherence to a common workflow.
1650
1651 Returns None if no issue for this branch, or one of the following keywords:
1652 * 'error' - error from review tool (including deleted issues)
1653 * 'unsent' - not sent for review
1654 * 'waiting' - waiting for review
1655 * 'reply' - waiting for owner to reply to review
1656 * 'lgtm' - LGTM from at least one approved reviewer
1657 * 'commit' - in the commit queue
1658 * 'closed' - closed
1659 """
1660 if not self.GetIssue():
1661 return None
1662
1663 try:
1664 props = self.GetIssueProperties()
1665 except urllib2.HTTPError:
1666 return 'error'
1667
1668 if props.get('closed'):
1669 # Issue is closed.
1670 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001671 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001672 # Issue is in the commit queue.
1673 return 'commit'
1674
1675 try:
1676 reviewers = self.GetApprovingReviewers()
1677 except urllib2.HTTPError:
1678 return 'error'
1679
1680 if reviewers:
1681 # Was LGTM'ed.
1682 return 'lgtm'
1683
1684 messages = props.get('messages') or []
1685
1686 if not messages:
1687 # No message was sent.
1688 return 'unsent'
1689 if messages[-1]['sender'] != props.get('owner_email'):
1690 # Non-LGTM reply from non-owner
1691 return 'reply'
1692 return 'waiting'
1693
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001694 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001695 return self.RpcServer().update_description(
1696 self.GetIssue(), self.description)
1697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001698 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001699 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001701 def SetFlag(self, flag, value):
1702 """Patchset must match."""
1703 if not self.GetPatchset():
1704 DieWithError('The patchset needs to match. Send another patchset.')
1705 try:
1706 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001707 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001708 except urllib2.HTTPError, e:
1709 if e.code == 404:
1710 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1711 if e.code == 403:
1712 DieWithError(
1713 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1714 'match?') % (self.GetIssue(), self.GetPatchset()))
1715 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001717 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718 """Returns an upload.RpcServer() to access this review's rietveld instance.
1719 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001720 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001721 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001722 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001723 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001724 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001726 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001727 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001728 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001729
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001730 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731 """Return the git setting that stores this change's most recent patchset."""
1732 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1733
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001734 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001736 branch = self.GetBranch()
1737 if branch:
1738 return 'branch.%s.rietveldserver' % branch
1739 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 def GetRieveldObjForPresubmit(self):
1742 return self.RpcServer()
1743
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001744 def SetCQState(self, new_state):
1745 props = self.GetIssueProperties()
1746 if props.get('private'):
1747 DieWithError('Cannot set-commit on private issue')
1748
1749 if new_state == _CQState.COMMIT:
1750 self.SetFlag('commit', '1')
1751 elif new_state == _CQState.NONE:
1752 self.SetFlag('commit', '0')
1753 else:
1754 raise NotImplementedError()
1755
1756
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001757 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1758 directory):
1759 # TODO(maruel): Use apply_issue.py
1760
1761 # PatchIssue should never be called with a dirty tree. It is up to the
1762 # caller to check this, but just in case we assert here since the
1763 # consequences of the caller not checking this could be dire.
1764 assert(not git_common.is_dirty_git_tree('apply'))
1765 assert(parsed_issue_arg.valid)
1766 self._changelist.issue = parsed_issue_arg.issue
1767 if parsed_issue_arg.hostname:
1768 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1769
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001770 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1771 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001772 assert parsed_issue_arg.patchset
1773 patchset = parsed_issue_arg.patchset
1774 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1775 else:
1776 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1777 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1778
1779 # Switch up to the top-level directory, if necessary, in preparation for
1780 # applying the patch.
1781 top = settings.GetRelativeRoot()
1782 if top:
1783 os.chdir(top)
1784
1785 # Git patches have a/ at the beginning of source paths. We strip that out
1786 # with a sed script rather than the -p flag to patch so we can feed either
1787 # Git or svn-style patches into the same apply command.
1788 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1789 try:
1790 patch_data = subprocess2.check_output(
1791 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1792 except subprocess2.CalledProcessError:
1793 DieWithError('Git patch mungling failed.')
1794 logging.info(patch_data)
1795
1796 # We use "git apply" to apply the patch instead of "patch" so that we can
1797 # pick up file adds.
1798 # The --index flag means: also insert into the index (so we catch adds).
1799 cmd = ['git', 'apply', '--index', '-p0']
1800 if directory:
1801 cmd.extend(('--directory', directory))
1802 if reject:
1803 cmd.append('--reject')
1804 elif IsGitVersionAtLeast('1.7.12'):
1805 cmd.append('--3way')
1806 try:
1807 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1808 stdin=patch_data, stdout=subprocess2.VOID)
1809 except subprocess2.CalledProcessError:
1810 print 'Failed to apply the patch'
1811 return 1
1812
1813 # If we had an issue, commit the current state and register the issue.
1814 if not nocommit:
1815 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1816 'patch from issue %(i)s at patchset '
1817 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1818 % {'i': self.GetIssue(), 'p': patchset})])
1819 self.SetIssue(self.GetIssue())
1820 self.SetPatchset(patchset)
1821 print "Committed patch locally."
1822 else:
1823 print "Patch applied to index."
1824 return 0
1825
1826 @staticmethod
1827 def ParseIssueURL(parsed_url):
1828 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1829 return None
1830 # Typical url: https://domain/<issue_number>[/[other]]
1831 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1832 if match:
1833 return _RietveldParsedIssueNumberArgument(
1834 issue=int(match.group(1)),
1835 hostname=parsed_url.netloc)
1836 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1837 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1838 if match:
1839 return _RietveldParsedIssueNumberArgument(
1840 issue=int(match.group(1)),
1841 patchset=int(match.group(2)),
1842 hostname=parsed_url.netloc,
1843 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1844 return None
1845
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001846 def CMDUploadChange(self, options, args, change):
1847 """Upload the patch to Rietveld."""
1848 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1849 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001850 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1851 if options.emulate_svn_auto_props:
1852 upload_args.append('--emulate_svn_auto_props')
1853
1854 change_desc = None
1855
1856 if options.email is not None:
1857 upload_args.extend(['--email', options.email])
1858
1859 if self.GetIssue():
1860 if options.title:
1861 upload_args.extend(['--title', options.title])
1862 if options.message:
1863 upload_args.extend(['--message', options.message])
1864 upload_args.extend(['--issue', str(self.GetIssue())])
1865 print ('This branch is associated with issue %s. '
1866 'Adding patch to that issue.' % self.GetIssue())
1867 else:
1868 if options.title:
1869 upload_args.extend(['--title', options.title])
1870 message = (options.title or options.message or
1871 CreateDescriptionFromLog(args))
1872 change_desc = ChangeDescription(message)
1873 if options.reviewers or options.tbr_owners:
1874 change_desc.update_reviewers(options.reviewers,
1875 options.tbr_owners,
1876 change)
1877 if not options.force:
1878 change_desc.prompt()
1879
1880 if not change_desc.description:
1881 print "Description is empty; aborting."
1882 return 1
1883
1884 upload_args.extend(['--message', change_desc.description])
1885 if change_desc.get_reviewers():
1886 upload_args.append('--reviewers=%s' % ','.join(
1887 change_desc.get_reviewers()))
1888 if options.send_mail:
1889 if not change_desc.get_reviewers():
1890 DieWithError("Must specify reviewers to send email.")
1891 upload_args.append('--send_mail')
1892
1893 # We check this before applying rietveld.private assuming that in
1894 # rietveld.cc only addresses which we can send private CLs to are listed
1895 # if rietveld.private is set, and so we should ignore rietveld.cc only
1896 # when --private is specified explicitly on the command line.
1897 if options.private:
1898 logging.warn('rietveld.cc is ignored since private flag is specified. '
1899 'You need to review and add them manually if necessary.')
1900 cc = self.GetCCListWithoutDefault()
1901 else:
1902 cc = self.GetCCList()
1903 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1904 if cc:
1905 upload_args.extend(['--cc', cc])
1906
1907 if options.private or settings.GetDefaultPrivateFlag() == "True":
1908 upload_args.append('--private')
1909
1910 upload_args.extend(['--git_similarity', str(options.similarity)])
1911 if not options.find_copies:
1912 upload_args.extend(['--git_no_find_copies'])
1913
1914 # Include the upstream repo's URL in the change -- this is useful for
1915 # projects that have their source spread across multiple repos.
1916 remote_url = self.GetGitBaseUrlFromConfig()
1917 if not remote_url:
1918 if settings.GetIsGitSvn():
1919 remote_url = self.GetGitSvnRemoteUrl()
1920 else:
1921 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1922 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1923 self.GetUpstreamBranch().split('/')[-1])
1924 if remote_url:
1925 upload_args.extend(['--base_url', remote_url])
1926 remote, remote_branch = self.GetRemoteBranch()
1927 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1928 settings.GetPendingRefPrefix())
1929 if target_ref:
1930 upload_args.extend(['--target_ref', target_ref])
1931
1932 # Look for dependent patchsets. See crbug.com/480453 for more details.
1933 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1934 upstream_branch = ShortBranchName(upstream_branch)
1935 if remote is '.':
1936 # A local branch is being tracked.
1937 local_branch = ShortBranchName(upstream_branch)
1938 if settings.GetIsSkipDependencyUpload(local_branch):
1939 print
1940 print ('Skipping dependency patchset upload because git config '
1941 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1942 print
1943 else:
1944 auth_config = auth.extract_auth_config_from_options(options)
1945 branch_cl = Changelist(branchref=local_branch,
1946 auth_config=auth_config)
1947 branch_cl_issue_url = branch_cl.GetIssueURL()
1948 branch_cl_issue = branch_cl.GetIssue()
1949 branch_cl_patchset = branch_cl.GetPatchset()
1950 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1951 upload_args.extend(
1952 ['--depends_on_patchset', '%s:%s' % (
1953 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001954 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001955 '\n'
1956 'The current branch (%s) is tracking a local branch (%s) with '
1957 'an associated CL.\n'
1958 'Adding %s/#ps%s as a dependency patchset.\n'
1959 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1960 branch_cl_patchset))
1961
1962 project = settings.GetProject()
1963 if project:
1964 upload_args.extend(['--project', project])
1965
1966 if options.cq_dry_run:
1967 upload_args.extend(['--cq_dry_run'])
1968
1969 try:
1970 upload_args = ['upload'] + upload_args + args
1971 logging.info('upload.RealMain(%s)', upload_args)
1972 issue, patchset = upload.RealMain(upload_args)
1973 issue = int(issue)
1974 patchset = int(patchset)
1975 except KeyboardInterrupt:
1976 sys.exit(1)
1977 except:
1978 # If we got an exception after the user typed a description for their
1979 # change, back up the description before re-raising.
1980 if change_desc:
1981 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1982 print('\nGot exception while uploading -- saving description to %s\n' %
1983 backup_path)
1984 backup_file = open(backup_path, 'w')
1985 backup_file.write(change_desc.description)
1986 backup_file.close()
1987 raise
1988
1989 if not self.GetIssue():
1990 self.SetIssue(issue)
1991 self.SetPatchset(patchset)
1992
1993 if options.use_commit_queue:
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001994 self.SetCQState(_CQState.COMMIT)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001995 return 0
1996
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001997
1998class _GerritChangelistImpl(_ChangelistCodereviewBase):
1999 def __init__(self, changelist, auth_config=None):
2000 # auth_config is Rietveld thing, kept here to preserve interface only.
2001 super(_GerritChangelistImpl, self).__init__(changelist)
2002 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002003 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002004 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002005 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002006
2007 def _GetGerritHost(self):
2008 # Lazy load of configs.
2009 self.GetCodereviewServer()
2010 return self._gerrit_host
2011
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002012 def _GetGitHost(self):
2013 """Returns git host to be used when uploading change to Gerrit."""
2014 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2015
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002016 def GetCodereviewServer(self):
2017 if not self._gerrit_server:
2018 # If we're on a branch then get the server potentially associated
2019 # with that branch.
2020 if self.GetIssue():
2021 gerrit_server_setting = self.GetCodereviewServerSetting()
2022 if gerrit_server_setting:
2023 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2024 error_ok=True).strip()
2025 if self._gerrit_server:
2026 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2027 if not self._gerrit_server:
2028 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2029 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002030 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002031 parts[0] = parts[0] + '-review'
2032 self._gerrit_host = '.'.join(parts)
2033 self._gerrit_server = 'https://%s' % self._gerrit_host
2034 return self._gerrit_server
2035
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002036 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00002037 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002038 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002039
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002040 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002041 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002042 if settings.GetGerritSkipEnsureAuthenticated():
2043 # For projects with unusual authentication schemes.
2044 # See http://crbug.com/603378.
2045 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002046 # Lazy-loader to identify Gerrit and Git hosts.
2047 if gerrit_util.GceAuthenticator.is_gce():
2048 return
2049 self.GetCodereviewServer()
2050 git_host = self._GetGitHost()
2051 assert self._gerrit_server and self._gerrit_host
2052 cookie_auth = gerrit_util.CookiesAuthenticator()
2053
2054 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2055 git_auth = cookie_auth.get_auth_header(git_host)
2056 if gerrit_auth and git_auth:
2057 if gerrit_auth == git_auth:
2058 return
2059 print((
2060 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2061 ' Check your %s or %s file for credentials of hosts:\n'
2062 ' %s\n'
2063 ' %s\n'
2064 ' %s') %
2065 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2066 git_host, self._gerrit_host,
2067 cookie_auth.get_new_password_message(git_host)))
2068 if not force:
2069 ask_for_data('If you know what you are doing, press Enter to continue, '
2070 'Ctrl+C to abort.')
2071 return
2072 else:
2073 missing = (
2074 [] if gerrit_auth else [self._gerrit_host] +
2075 [] if git_auth else [git_host])
2076 DieWithError('Credentials for the following hosts are required:\n'
2077 ' %s\n'
2078 'These are read from %s (or legacy %s)\n'
2079 '%s' % (
2080 '\n '.join(missing),
2081 cookie_auth.get_gitcookies_path(),
2082 cookie_auth.get_netrc_path(),
2083 cookie_auth.get_new_password_message(git_host)))
2084
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002085
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002086 def PatchsetSetting(self):
2087 """Return the git setting that stores this change's most recent patchset."""
2088 return 'branch.%s.gerritpatchset' % self.GetBranch()
2089
2090 def GetCodereviewServerSetting(self):
2091 """Returns the git setting that stores this change's Gerrit server."""
2092 branch = self.GetBranch()
2093 if branch:
2094 return 'branch.%s.gerritserver' % branch
2095 return None
2096
2097 def GetRieveldObjForPresubmit(self):
2098 class ThisIsNotRietveldIssue(object):
2099 def __nonzero__(self):
2100 # This is a hack to make presubmit_support think that rietveld is not
2101 # defined, yet still ensure that calls directly result in a decent
2102 # exception message below.
2103 return False
2104
2105 def __getattr__(self, attr):
2106 print(
2107 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2108 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2109 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2110 'or use Rietveld for codereview.\n'
2111 'See also http://crbug.com/579160.' % attr)
2112 raise NotImplementedError()
2113 return ThisIsNotRietveldIssue()
2114
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002115 def GetGerritObjForPresubmit(self):
2116 return presubmit_support.GerritAccessor(self._GetGerritHost())
2117
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002118 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002119 """Apply a rough heuristic to give a simple summary of an issue's review
2120 or CQ status, assuming adherence to a common workflow.
2121
2122 Returns None if no issue for this branch, or one of the following keywords:
2123 * 'error' - error from review tool (including deleted issues)
2124 * 'unsent' - no reviewers added
2125 * 'waiting' - waiting for review
2126 * 'reply' - waiting for owner to reply to review
2127 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2128 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2129 * 'commit' - in the commit queue
2130 * 'closed' - abandoned
2131 """
2132 if not self.GetIssue():
2133 return None
2134
2135 try:
2136 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2137 except httplib.HTTPException:
2138 return 'error'
2139
2140 if data['status'] == 'ABANDONED':
2141 return 'closed'
2142
2143 cq_label = data['labels'].get('Commit-Queue', {})
2144 if cq_label:
2145 # Vote value is a stringified integer, which we expect from 0 to 2.
2146 vote_value = cq_label.get('value', '0')
2147 vote_text = cq_label.get('values', {}).get(vote_value, '')
2148 if vote_text.lower() == 'commit':
2149 return 'commit'
2150
2151 lgtm_label = data['labels'].get('Code-Review', {})
2152 if lgtm_label:
2153 if 'rejected' in lgtm_label:
2154 return 'not lgtm'
2155 if 'approved' in lgtm_label:
2156 return 'lgtm'
2157
2158 if not data.get('reviewers', {}).get('REVIEWER', []):
2159 return 'unsent'
2160
2161 messages = data.get('messages', [])
2162 if messages:
2163 owner = data['owner'].get('_account_id')
2164 last_message_author = messages[-1].get('author', {}).get('_account_id')
2165 if owner != last_message_author:
2166 # Some reply from non-owner.
2167 return 'reply'
2168
2169 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002170
2171 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002172 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002173 return data['revisions'][data['current_revision']]['_number']
2174
2175 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002176 data = self._GetChangeDetail(['CURRENT_REVISION'])
2177 current_rev = data['current_revision']
2178 url = data['revisions'][current_rev]['fetch']['http']['url']
2179 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002180
2181 def UpdateDescriptionRemote(self, description):
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002182 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2183 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002184
2185 def CloseIssue(self):
2186 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2187
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002188 def GetApprovingReviewers(self):
2189 """Returns a list of reviewers approving the change.
2190
2191 Note: not necessarily committers.
2192 """
2193 raise NotImplementedError()
2194
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002195 def SubmitIssue(self, wait_for_merge=True):
2196 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2197 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002198
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002199 def _GetChangeDetail(self, options=None, issue=None):
2200 options = options or []
2201 issue = issue or self.GetIssue()
2202 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002203 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2204 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002205
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002206 def CMDLand(self, force, bypass_hooks, verbose):
2207 if git_common.is_dirty_git_tree('land'):
2208 return 1
2209 differs = True
2210 last_upload = RunGit(['config',
2211 'branch.%s.gerritsquashhash' % self.GetBranch()],
2212 error_ok=True).strip()
2213 # Note: git diff outputs nothing if there is no diff.
2214 if not last_upload or RunGit(['diff', last_upload]).strip():
2215 print('WARNING: some changes from local branch haven\'t been uploaded')
2216 else:
2217 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2218 if detail['current_revision'] == last_upload:
2219 differs = False
2220 else:
2221 print('WARNING: local branch contents differ from latest uploaded '
2222 'patchset')
2223 if differs:
2224 if not force:
2225 ask_for_data(
2226 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2227 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2228 elif not bypass_hooks:
2229 hook_results = self.RunHook(
2230 committing=True,
2231 may_prompt=not force,
2232 verbose=verbose,
2233 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2234 if not hook_results.should_continue():
2235 return 1
2236
2237 self.SubmitIssue(wait_for_merge=True)
2238 print('Issue %s has been submitted.' % self.GetIssueURL())
2239 return 0
2240
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002241 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2242 directory):
2243 assert not reject
2244 assert not nocommit
2245 assert not directory
2246 assert parsed_issue_arg.valid
2247
2248 self._changelist.issue = parsed_issue_arg.issue
2249
2250 if parsed_issue_arg.hostname:
2251 self._gerrit_host = parsed_issue_arg.hostname
2252 self._gerrit_server = 'https://%s' % self._gerrit_host
2253
2254 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2255
2256 if not parsed_issue_arg.patchset:
2257 # Use current revision by default.
2258 revision_info = detail['revisions'][detail['current_revision']]
2259 patchset = int(revision_info['_number'])
2260 else:
2261 patchset = parsed_issue_arg.patchset
2262 for revision_info in detail['revisions'].itervalues():
2263 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2264 break
2265 else:
2266 DieWithError('Couldn\'t find patchset %i in issue %i' %
2267 (parsed_issue_arg.patchset, self.GetIssue()))
2268
2269 fetch_info = revision_info['fetch']['http']
2270 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2271 RunGit(['cherry-pick', 'FETCH_HEAD'])
2272 self.SetIssue(self.GetIssue())
2273 self.SetPatchset(patchset)
2274 print('Committed patch for issue %i pathset %i locally' %
2275 (self.GetIssue(), self.GetPatchset()))
2276 return 0
2277
2278 @staticmethod
2279 def ParseIssueURL(parsed_url):
2280 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2281 return None
2282 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2283 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2284 # Short urls like https://domain/<issue_number> can be used, but don't allow
2285 # specifying the patchset (you'd 404), but we allow that here.
2286 if parsed_url.path == '/':
2287 part = parsed_url.fragment
2288 else:
2289 part = parsed_url.path
2290 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2291 if match:
2292 return _ParsedIssueNumberArgument(
2293 issue=int(match.group(2)),
2294 patchset=int(match.group(4)) if match.group(4) else None,
2295 hostname=parsed_url.netloc)
2296 return None
2297
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002298 def CMDUploadChange(self, options, args, change):
2299 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002300 if options.squash and options.no_squash:
2301 DieWithError('Can only use one of --squash or --no-squash')
2302 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2303 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002304 # We assume the remote called "origin" is the one we want.
2305 # It is probably not worthwhile to support different workflows.
2306 gerrit_remote = 'origin'
2307
2308 remote, remote_branch = self.GetRemoteBranch()
2309 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2310 pending_prefix='')
2311
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002312 if options.squash:
2313 if not self.GetIssue():
2314 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2315 # with shadow branch, which used to contain change-id for a given
2316 # branch, using which we can fetch actual issue number and set it as the
2317 # property of the branch, which is the new way.
2318 message = RunGitSilent([
2319 'show', '--format=%B', '-s',
2320 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2321 if message:
2322 change_ids = git_footers.get_footer_change_id(message.strip())
2323 if change_ids and len(change_ids) == 1:
2324 details = self._GetChangeDetail(issue=change_ids[0])
2325 if details:
2326 print('WARNING: found old upload in branch git_cl_uploads/%s '
2327 'corresponding to issue %s' %
2328 (self.GetBranch(), details['_number']))
2329 self.SetIssue(details['_number'])
2330 if not self.GetIssue():
2331 DieWithError(
2332 '\n' # For readability of the blob below.
2333 'Found old upload in branch git_cl_uploads/%s, '
2334 'but failed to find corresponding Gerrit issue.\n'
2335 'If you know the issue number, set it manually first:\n'
2336 ' git cl issue 123456\n'
2337 'If you intended to upload this CL as new issue, '
2338 'just delete or rename the old upload branch:\n'
2339 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2340 'After that, please run git cl upload again.' %
2341 tuple([self.GetBranch()] * 3))
2342 # End of backwards compatability.
2343
2344 if self.GetIssue():
2345 # Try to get the message from a previous upload.
2346 message = self.GetDescription()
2347 if not message:
2348 DieWithError(
2349 'failed to fetch description from current Gerrit issue %d\n'
2350 '%s' % (self.GetIssue(), self.GetIssueURL()))
2351 change_id = self._GetChangeDetail()['change_id']
2352 while True:
2353 footer_change_ids = git_footers.get_footer_change_id(message)
2354 if footer_change_ids == [change_id]:
2355 break
2356 if not footer_change_ids:
2357 message = git_footers.add_footer_change_id(message, change_id)
2358 print('WARNING: appended missing Change-Id to issue description')
2359 continue
2360 # There is already a valid footer but with different or several ids.
2361 # Doing this automatically is non-trivial as we don't want to lose
2362 # existing other footers, yet we want to append just 1 desired
2363 # Change-Id. Thus, just create a new footer, but let user verify the
2364 # new description.
2365 message = '%s\n\nChange-Id: %s' % (message, change_id)
2366 print(
2367 'WARNING: issue %s has Change-Id footer(s):\n'
2368 ' %s\n'
2369 'but issue has Change-Id %s, according to Gerrit.\n'
2370 'Please, check the proposed correction to the description, '
2371 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2372 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2373 change_id))
2374 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2375 if not options.force:
2376 change_desc = ChangeDescription(message)
2377 change_desc.prompt()
2378 message = change_desc.description
2379 if not message:
2380 DieWithError("Description is empty. Aborting...")
2381 # Continue the while loop.
2382 # Sanity check of this code - we should end up with proper message
2383 # footer.
2384 assert [change_id] == git_footers.get_footer_change_id(message)
2385 change_desc = ChangeDescription(message)
2386 else:
2387 change_desc = ChangeDescription(
2388 options.message or CreateDescriptionFromLog(args))
2389 if not options.force:
2390 change_desc.prompt()
2391 if not change_desc.description:
2392 DieWithError("Description is empty. Aborting...")
2393 message = change_desc.description
2394 change_ids = git_footers.get_footer_change_id(message)
2395 if len(change_ids) > 1:
2396 DieWithError('too many Change-Id footers, at most 1 allowed.')
2397 if not change_ids:
2398 # Generate the Change-Id automatically.
2399 message = git_footers.add_footer_change_id(
2400 message, GenerateGerritChangeId(message))
2401 change_desc.set_description(message)
2402 change_ids = git_footers.get_footer_change_id(message)
2403 assert len(change_ids) == 1
2404 change_id = change_ids[0]
2405
2406 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2407 if remote is '.':
2408 # If our upstream branch is local, we base our squashed commit on its
2409 # squashed version.
2410 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2411 # Check the squashed hash of the parent.
2412 parent = RunGit(['config',
2413 'branch.%s.gerritsquashhash' % upstream_branch_name],
2414 error_ok=True).strip()
2415 # Verify that the upstream branch has been uploaded too, otherwise
2416 # Gerrit will create additional CLs when uploading.
2417 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2418 RunGitSilent(['rev-parse', parent + ':'])):
2419 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2420 DieWithError(
2421 'Upload upstream branch %s first.\n'
2422 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2423 'version of depot_tools. If so, then re-upload it with:\n'
2424 ' git cl upload --squash\n' % upstream_branch_name)
2425 else:
2426 parent = self.GetCommonAncestorWithUpstream()
2427
2428 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2429 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2430 '-m', message]).strip()
2431 else:
2432 change_desc = ChangeDescription(
2433 options.message or CreateDescriptionFromLog(args))
2434 if not change_desc.description:
2435 DieWithError("Description is empty. Aborting...")
2436
2437 if not git_footers.get_footer_change_id(change_desc.description):
2438 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002439 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2440 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002441 ref_to_push = 'HEAD'
2442 parent = '%s/%s' % (gerrit_remote, branch)
2443 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2444
2445 assert change_desc
2446 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2447 ref_to_push)]).splitlines()
2448 if len(commits) > 1:
2449 print('WARNING: This will upload %d commits. Run the following command '
2450 'to see which commits will be uploaded: ' % len(commits))
2451 print('git log %s..%s' % (parent, ref_to_push))
2452 print('You can also use `git squash-branch` to squash these into a '
2453 'single commit.')
2454 ask_for_data('About to upload; enter to confirm.')
2455
2456 if options.reviewers or options.tbr_owners:
2457 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2458 change)
2459
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002460 # Extra options that can be specified at push time. Doc:
2461 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2462 refspec_opts = []
2463 if options.title:
2464 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2465 # reverse on its side.
2466 if '_' in options.title:
2467 print('WARNING: underscores in title will be converted to spaces.')
2468 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2469
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002470 cc = self.GetCCList().split(',')
2471 if options.cc:
2472 cc.extend(options.cc)
2473 cc = filter(None, cc)
2474 if cc:
tandrii@chromium.org0b2d7072016-04-18 16:19:03 +00002475 # refspec_opts.extend('cc=' + email.strip() for email in cc)
2476 # TODO(tandrii): enable this back. http://crbug.com/604377
2477 print('WARNING: Gerrit doesn\'t yet support cc-ing arbitrary emails.\n'
2478 ' Ignoring cc-ed emails. See http://crbug.com/604377.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002479
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002480 if change_desc.get_reviewers():
2481 refspec_opts.extend('r=' + email.strip()
2482 for email in change_desc.get_reviewers())
2483
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002484
2485 refspec_suffix = ''
2486 if refspec_opts:
2487 refspec_suffix = '%' + ','.join(refspec_opts)
2488 assert ' ' not in refspec_suffix, (
2489 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002490 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002491
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002492 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002493 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002494 print_stdout=True,
2495 # Flush after every line: useful for seeing progress when running as
2496 # recipe.
2497 filter_fn=lambda _: sys.stdout.flush())
2498
2499 if options.squash:
2500 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2501 change_numbers = [m.group(1)
2502 for m in map(regex.match, push_stdout.splitlines())
2503 if m]
2504 if len(change_numbers) != 1:
2505 DieWithError(
2506 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2507 'Change-Id: %s') % (len(change_numbers), change_id))
2508 self.SetIssue(change_numbers[0])
2509 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2510 ref_to_push])
2511 return 0
2512
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002513 def _AddChangeIdToCommitMessage(self, options, args):
2514 """Re-commits using the current message, assumes the commit hook is in
2515 place.
2516 """
2517 log_desc = options.message or CreateDescriptionFromLog(args)
2518 git_command = ['commit', '--amend', '-m', log_desc]
2519 RunGit(git_command)
2520 new_log_desc = CreateDescriptionFromLog(args)
2521 if git_footers.get_footer_change_id(new_log_desc):
2522 print 'git-cl: Added Change-Id to commit message.'
2523 return new_log_desc
2524 else:
2525 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002526
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002527 def SetCQState(self, new_state):
2528 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2529 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2530 # self-discovery of label config for this CL using REST API.
2531 vote_map = {
2532 _CQState.NONE: 0,
2533 _CQState.DRY_RUN: 1,
2534 _CQState.COMMIT : 2,
2535 }
2536 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2537 labels={'Commit-Queue': vote_map[new_state]})
2538
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002539
2540_CODEREVIEW_IMPLEMENTATIONS = {
2541 'rietveld': _RietveldChangelistImpl,
2542 'gerrit': _GerritChangelistImpl,
2543}
2544
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002545
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002546def _add_codereview_select_options(parser):
2547 """Appends --gerrit and --rietveld options to force specific codereview."""
2548 parser.codereview_group = optparse.OptionGroup(
2549 parser, 'EXPERIMENTAL! Codereview override options')
2550 parser.add_option_group(parser.codereview_group)
2551 parser.codereview_group.add_option(
2552 '--gerrit', action='store_true',
2553 help='Force the use of Gerrit for codereview')
2554 parser.codereview_group.add_option(
2555 '--rietveld', action='store_true',
2556 help='Force the use of Rietveld for codereview')
2557
2558
2559def _process_codereview_select_options(parser, options):
2560 if options.gerrit and options.rietveld:
2561 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2562 options.forced_codereview = None
2563 if options.gerrit:
2564 options.forced_codereview = 'gerrit'
2565 elif options.rietveld:
2566 options.forced_codereview = 'rietveld'
2567
2568
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002569class ChangeDescription(object):
2570 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002571 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002572 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002573
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002574 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002575 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002576
agable@chromium.org42c20792013-09-12 17:34:49 +00002577 @property # www.logilab.org/ticket/89786
2578 def description(self): # pylint: disable=E0202
2579 return '\n'.join(self._description_lines)
2580
2581 def set_description(self, desc):
2582 if isinstance(desc, basestring):
2583 lines = desc.splitlines()
2584 else:
2585 lines = [line.rstrip() for line in desc]
2586 while lines and not lines[0]:
2587 lines.pop(0)
2588 while lines and not lines[-1]:
2589 lines.pop(-1)
2590 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002591
piman@chromium.org336f9122014-09-04 02:16:55 +00002592 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002593 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002594 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002595 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002596 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002597 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002598
agable@chromium.org42c20792013-09-12 17:34:49 +00002599 # Get the set of R= and TBR= lines and remove them from the desciption.
2600 regexp = re.compile(self.R_LINE)
2601 matches = [regexp.match(line) for line in self._description_lines]
2602 new_desc = [l for i, l in enumerate(self._description_lines)
2603 if not matches[i]]
2604 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002605
agable@chromium.org42c20792013-09-12 17:34:49 +00002606 # Construct new unified R= and TBR= lines.
2607 r_names = []
2608 tbr_names = []
2609 for match in matches:
2610 if not match:
2611 continue
2612 people = cleanup_list([match.group(2).strip()])
2613 if match.group(1) == 'TBR':
2614 tbr_names.extend(people)
2615 else:
2616 r_names.extend(people)
2617 for name in r_names:
2618 if name not in reviewers:
2619 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002620 if add_owners_tbr:
2621 owners_db = owners.Database(change.RepositoryRoot(),
2622 fopen=file, os_path=os.path, glob=glob.glob)
2623 all_reviewers = set(tbr_names + reviewers)
2624 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2625 all_reviewers)
2626 tbr_names.extend(owners_db.reviewers_for(missing_files,
2627 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002628 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2629 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2630
2631 # Put the new lines in the description where the old first R= line was.
2632 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2633 if 0 <= line_loc < len(self._description_lines):
2634 if new_tbr_line:
2635 self._description_lines.insert(line_loc, new_tbr_line)
2636 if new_r_line:
2637 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002638 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002639 if new_r_line:
2640 self.append_footer(new_r_line)
2641 if new_tbr_line:
2642 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002643
2644 def prompt(self):
2645 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002646 self.set_description([
2647 '# Enter a description of the change.',
2648 '# This will be displayed on the codereview site.',
2649 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002650 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002651 '--------------------',
2652 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002653
agable@chromium.org42c20792013-09-12 17:34:49 +00002654 regexp = re.compile(self.BUG_LINE)
2655 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002656 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002657 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002658 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002659 if not content:
2660 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002661 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002662
2663 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002664 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2665 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002666 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002667 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002668
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002669 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002670 if self._description_lines:
2671 # Add an empty line if either the last line or the new line isn't a tag.
2672 last_line = self._description_lines[-1]
2673 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2674 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2675 self._description_lines.append('')
2676 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002677
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002678 def get_reviewers(self):
2679 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002680 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2681 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002682 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002683
2684
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002685def get_approving_reviewers(props):
2686 """Retrieves the reviewers that approved a CL from the issue properties with
2687 messages.
2688
2689 Note that the list may contain reviewers that are not committer, thus are not
2690 considered by the CQ.
2691 """
2692 return sorted(
2693 set(
2694 message['sender']
2695 for message in props['messages']
2696 if message['approval'] and message['sender'] in props['reviewers']
2697 )
2698 )
2699
2700
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002701def FindCodereviewSettingsFile(filename='codereview.settings'):
2702 """Finds the given file starting in the cwd and going up.
2703
2704 Only looks up to the top of the repository unless an
2705 'inherit-review-settings-ok' file exists in the root of the repository.
2706 """
2707 inherit_ok_file = 'inherit-review-settings-ok'
2708 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002709 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002710 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2711 root = '/'
2712 while True:
2713 if filename in os.listdir(cwd):
2714 if os.path.isfile(os.path.join(cwd, filename)):
2715 return open(os.path.join(cwd, filename))
2716 if cwd == root:
2717 break
2718 cwd = os.path.dirname(cwd)
2719
2720
2721def LoadCodereviewSettingsFromFile(fileobj):
2722 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002723 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002724
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002725 def SetProperty(name, setting, unset_error_ok=False):
2726 fullname = 'rietveld.' + name
2727 if setting in keyvals:
2728 RunGit(['config', fullname, keyvals[setting]])
2729 else:
2730 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2731
2732 SetProperty('server', 'CODE_REVIEW_SERVER')
2733 # Only server setting is required. Other settings can be absent.
2734 # In that case, we ignore errors raised during option deletion attempt.
2735 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002736 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002737 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2738 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002739 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002740 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002741 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2742 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002743 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002744 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002745 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002746 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2747 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002748
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002749 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002750 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002751
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002752 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2753 RunGit(['config', 'gerrit.squash-uploads',
2754 keyvals['GERRIT_SQUASH_UPLOADS']])
2755
tandrii@chromium.org28253532016-04-14 13:46:56 +00002756 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00002757 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00002758 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002760 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2761 #should be of the form
2762 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2763 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2764 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2765 keyvals['ORIGIN_URL_CONFIG']])
2766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002767
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002768def urlretrieve(source, destination):
2769 """urllib is broken for SSL connections via a proxy therefore we
2770 can't use urllib.urlretrieve()."""
2771 with open(destination, 'w') as f:
2772 f.write(urllib2.urlopen(source).read())
2773
2774
ukai@chromium.org712d6102013-11-27 00:52:58 +00002775def hasSheBang(fname):
2776 """Checks fname is a #! script."""
2777 with open(fname) as f:
2778 return f.read(2).startswith('#!')
2779
2780
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002781# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2782def DownloadHooks(*args, **kwargs):
2783 pass
2784
2785
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002786def DownloadGerritHook(force):
2787 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002788
2789 Args:
2790 force: True to update hooks. False to install hooks if not present.
2791 """
2792 if not settings.GetIsGerrit():
2793 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002794 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002795 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2796 if not os.access(dst, os.X_OK):
2797 if os.path.exists(dst):
2798 if not force:
2799 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002800 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002801 print(
2802 'WARNING: installing Gerrit commit-msg hook.\n'
2803 ' This behavior of git cl will soon be disabled.\n'
2804 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002805 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002806 if not hasSheBang(dst):
2807 DieWithError('Not a script: %s\n'
2808 'You need to download from\n%s\n'
2809 'into .git/hooks/commit-msg and '
2810 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002811 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2812 except Exception:
2813 if os.path.exists(dst):
2814 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002815 DieWithError('\nFailed to download hooks.\n'
2816 'You need to download from\n%s\n'
2817 'into .git/hooks/commit-msg and '
2818 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002819
2820
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002821
2822def GetRietveldCodereviewSettingsInteractively():
2823 """Prompt the user for settings."""
2824 server = settings.GetDefaultServerUrl(error_ok=True)
2825 prompt = 'Rietveld server (host[:port])'
2826 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2827 newserver = ask_for_data(prompt + ':')
2828 if not server and not newserver:
2829 newserver = DEFAULT_SERVER
2830 if newserver:
2831 newserver = gclient_utils.UpgradeToHttps(newserver)
2832 if newserver != server:
2833 RunGit(['config', 'rietveld.server', newserver])
2834
2835 def SetProperty(initial, caption, name, is_url):
2836 prompt = caption
2837 if initial:
2838 prompt += ' ("x" to clear) [%s]' % initial
2839 new_val = ask_for_data(prompt + ':')
2840 if new_val == 'x':
2841 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2842 elif new_val:
2843 if is_url:
2844 new_val = gclient_utils.UpgradeToHttps(new_val)
2845 if new_val != initial:
2846 RunGit(['config', 'rietveld.' + name, new_val])
2847
2848 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2849 SetProperty(settings.GetDefaultPrivateFlag(),
2850 'Private flag (rietveld only)', 'private', False)
2851 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2852 'tree-status-url', False)
2853 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2854 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2855 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2856 'run-post-upload-hook', False)
2857
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002858@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002860 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002861
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002862 print('WARNING: git cl config works for Rietveld only.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002863 'For Gerrit, see http://crbug.com/603116.')
2864 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002865 parser.add_option('--activate-update', action='store_true',
2866 help='activate auto-updating [rietveld] section in '
2867 '.git/config')
2868 parser.add_option('--deactivate-update', action='store_true',
2869 help='deactivate auto-updating [rietveld] section in '
2870 '.git/config')
2871 options, args = parser.parse_args(args)
2872
2873 if options.deactivate_update:
2874 RunGit(['config', 'rietveld.autoupdate', 'false'])
2875 return
2876
2877 if options.activate_update:
2878 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2879 return
2880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002881 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002882 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002883 return 0
2884
2885 url = args[0]
2886 if not url.endswith('codereview.settings'):
2887 url = os.path.join(url, 'codereview.settings')
2888
2889 # Load code review settings and download hooks (if available).
2890 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2891 return 0
2892
2893
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002894def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002895 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002896 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2897 branch = ShortBranchName(branchref)
2898 _, args = parser.parse_args(args)
2899 if not args:
2900 print("Current base-url:")
2901 return RunGit(['config', 'branch.%s.base-url' % branch],
2902 error_ok=False).strip()
2903 else:
2904 print("Setting base-url to %s" % args[0])
2905 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2906 error_ok=False).strip()
2907
2908
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002909def color_for_status(status):
2910 """Maps a Changelist status to color, for CMDstatus and other tools."""
2911 return {
2912 'unsent': Fore.RED,
2913 'waiting': Fore.BLUE,
2914 'reply': Fore.YELLOW,
2915 'lgtm': Fore.GREEN,
2916 'commit': Fore.MAGENTA,
2917 'closed': Fore.CYAN,
2918 'error': Fore.WHITE,
2919 }.get(status, Fore.WHITE)
2920
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002921def fetch_cl_status(branch, auth_config=None):
2922 """Fetches information for an issue and returns (branch, issue, status)."""
2923 cl = Changelist(branchref=branch, auth_config=auth_config)
2924 url = cl.GetIssueURL()
2925 status = cl.GetStatus()
2926
2927 if url and (not status or status == 'error'):
2928 # The issue probably doesn't exist anymore.
2929 url += ' (broken)'
2930
2931 return (branch, url, status)
2932
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002933def get_cl_statuses(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002934 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002935 """Returns a blocking iterable of (branch, issue, color) for given branches.
2936
2937 If fine_grained is true, this will fetch CL statuses from the server.
2938 Otherwise, simply indicate if there's a matching url for the given branches.
2939
2940 If max_processes is specified, it is used as the maximum number of processes
2941 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2942 spawned.
2943 """
2944 # Silence upload.py otherwise it becomes unwieldly.
2945 upload.verbosity = 0
2946
2947 if fine_grained:
2948 # Process one branch synchronously to work through authentication, then
2949 # spawn processes to process all the other branches in parallel.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002950 if branches:
2951 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2952 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002953
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002954 branches_to_fetch = branches[1:]
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002955 pool = ThreadPool(
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002956 min(max_processes, len(branches_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002957 if max_processes is not None
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002958 else len(branches_to_fetch))
2959 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002960 yield x
2961 else:
2962 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00002963 for b in branches:
2964 cl = Changelist(branchref=b, auth_config=auth_config)
2965 url = cl.GetIssueURL()
2966 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002967
rmistry@google.com2dd99862015-06-22 12:22:18 +00002968
2969def upload_branch_deps(cl, args):
2970 """Uploads CLs of local branches that are dependents of the current branch.
2971
2972 If the local branch dependency tree looks like:
2973 test1 -> test2.1 -> test3.1
2974 -> test3.2
2975 -> test2.2 -> test3.3
2976
2977 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2978 run on the dependent branches in this order:
2979 test2.1, test3.1, test3.2, test2.2, test3.3
2980
2981 Note: This function does not rebase your local dependent branches. Use it when
2982 you make a change to the parent branch that will not conflict with its
2983 dependent branches, and you would like their dependencies updated in
2984 Rietveld.
2985 """
2986 if git_common.is_dirty_git_tree('upload-branch-deps'):
2987 return 1
2988
2989 root_branch = cl.GetBranch()
2990 if root_branch is None:
2991 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2992 'Get on a branch!')
2993 if not cl.GetIssue() or not cl.GetPatchset():
2994 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2995 'patchset dependencies without an uploaded CL.')
2996
2997 branches = RunGit(['for-each-ref',
2998 '--format=%(refname:short) %(upstream:short)',
2999 'refs/heads'])
3000 if not branches:
3001 print('No local branches found.')
3002 return 0
3003
3004 # Create a dictionary of all local branches to the branches that are dependent
3005 # on it.
3006 tracked_to_dependents = collections.defaultdict(list)
3007 for b in branches.splitlines():
3008 tokens = b.split()
3009 if len(tokens) == 2:
3010 branch_name, tracked = tokens
3011 tracked_to_dependents[tracked].append(branch_name)
3012
3013 print
3014 print 'The dependent local branches of %s are:' % root_branch
3015 dependents = []
3016 def traverse_dependents_preorder(branch, padding=''):
3017 dependents_to_process = tracked_to_dependents.get(branch, [])
3018 padding += ' '
3019 for dependent in dependents_to_process:
3020 print '%s%s' % (padding, dependent)
3021 dependents.append(dependent)
3022 traverse_dependents_preorder(dependent, padding)
3023 traverse_dependents_preorder(root_branch)
3024 print
3025
3026 if not dependents:
3027 print 'There are no dependent local branches for %s' % root_branch
3028 return 0
3029
3030 print ('This command will checkout all dependent branches and run '
3031 '"git cl upload".')
3032 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3033
andybons@chromium.org962f9462016-02-03 20:00:42 +00003034 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003035 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003036 args.extend(['-t', 'Updated patchset dependency'])
3037
rmistry@google.com2dd99862015-06-22 12:22:18 +00003038 # Record all dependents that failed to upload.
3039 failures = {}
3040 # Go through all dependents, checkout the branch and upload.
3041 try:
3042 for dependent_branch in dependents:
3043 print
3044 print '--------------------------------------'
3045 print 'Running "git cl upload" from %s:' % dependent_branch
3046 RunGit(['checkout', '-q', dependent_branch])
3047 print
3048 try:
3049 if CMDupload(OptionParser(), args) != 0:
3050 print 'Upload failed for %s!' % dependent_branch
3051 failures[dependent_branch] = 1
3052 except: # pylint: disable=W0702
3053 failures[dependent_branch] = 1
3054 print
3055 finally:
3056 # Swap back to the original root branch.
3057 RunGit(['checkout', '-q', root_branch])
3058
3059 print
3060 print 'Upload complete for dependent branches!'
3061 for dependent_branch in dependents:
3062 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3063 print ' %s : %s' % (dependent_branch, upload_status)
3064 print
3065
3066 return 0
3067
3068
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003069def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003070 """Show status of changelists.
3071
3072 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003073 - Red not sent for review or broken
3074 - Blue waiting for review
3075 - Yellow waiting for you to reply to review
3076 - Green LGTM'ed
3077 - Magenta in the commit queue
3078 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003079
3080 Also see 'git cl comments'.
3081 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003082 parser.add_option('--field',
3083 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003084 parser.add_option('-f', '--fast', action='store_true',
3085 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003086 parser.add_option(
3087 '-j', '--maxjobs', action='store', type=int,
3088 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003089
3090 auth.add_auth_options(parser)
3091 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003092 if args:
3093 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003094 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003096 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003097 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003098 if options.field.startswith('desc'):
3099 print cl.GetDescription()
3100 elif options.field == 'id':
3101 issueid = cl.GetIssue()
3102 if issueid:
3103 print issueid
3104 elif options.field == 'patch':
3105 patchset = cl.GetPatchset()
3106 if patchset:
3107 print patchset
3108 elif options.field == 'url':
3109 url = cl.GetIssueURL()
3110 if url:
3111 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003112 return 0
3113
3114 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3115 if not branches:
3116 print('No local branch found.')
3117 return 0
3118
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003119 changes = (
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003120 Changelist(branchref=b, auth_config=auth_config)
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003121 for b in branches.splitlines())
3122 # TODO(tandrii): refactor to use CLs list instead of branches list.
3123 branches = [c.GetBranch() for c in changes]
3124 alignment = max(5, max(len(b) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003125 print 'Branches associated with reviews:'
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003126 output = get_cl_statuses(branches,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003127 fine_grained=not options.fast,
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003128 max_processes=options.maxjobs,
3129 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003130
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003131 branch_statuses = {}
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003132 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
3133 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003134 while branch not in branch_statuses:
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003135 b, i, status = output.next()
3136 branch_statuses[b] = (i, status)
3137 issue_url, status = branch_statuses.pop(branch)
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003138 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003139 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003140 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003141 color = ''
3142 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003143 status_str = '(%s)' % status if status else ''
3144 print ' %*s : %s%s %s%s' % (
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003145 alignment, ShortBranchName(branch), color, issue_url, status_str,
3146 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003147
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003148 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003149 print
3150 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003151 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003152 if not cl.GetIssue():
3153 print 'No issue assigned.'
3154 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003155 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003156 if not options.fast:
3157 print 'Issue description:'
3158 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003159 return 0
3160
3161
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003162def colorize_CMDstatus_doc():
3163 """To be called once in main() to add colors to git cl status help."""
3164 colors = [i for i in dir(Fore) if i[0].isupper()]
3165
3166 def colorize_line(line):
3167 for color in colors:
3168 if color in line.upper():
3169 # Extract whitespaces first and the leading '-'.
3170 indent = len(line) - len(line.lstrip(' ')) + 1
3171 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3172 return line
3173
3174 lines = CMDstatus.__doc__.splitlines()
3175 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3176
3177
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003178@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003179def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003180 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181
3182 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003183 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003184 parser.add_option('-r', '--reverse', action='store_true',
3185 help='Lookup the branch(es) for the specified issues. If '
3186 'no issues are specified, all branches with mapped '
3187 'issues will be listed.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003188 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003189 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003190 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191
dnj@chromium.org406c4402015-03-03 17:22:28 +00003192 if options.reverse:
3193 branches = RunGit(['for-each-ref', 'refs/heads',
3194 '--format=%(refname:short)']).splitlines()
3195
3196 # Reverse issue lookup.
3197 issue_branch_map = {}
3198 for branch in branches:
3199 cl = Changelist(branchref=branch)
3200 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3201 if not args:
3202 args = sorted(issue_branch_map.iterkeys())
3203 for issue in args:
3204 if not issue:
3205 continue
3206 print 'Branch for issue number %s: %s' % (
3207 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3208 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003209 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003210 if len(args) > 0:
3211 try:
3212 issue = int(args[0])
3213 except ValueError:
3214 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003215 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003216 cl.SetIssue(issue)
3217 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003218 return 0
3219
3220
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003221def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003222 """Shows or posts review comments for any changelist."""
3223 parser.add_option('-a', '--add-comment', dest='comment',
3224 help='comment to add to an issue')
3225 parser.add_option('-i', dest='issue',
3226 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003227 parser.add_option('-j', '--json-file',
3228 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003229 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003230 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003231 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003232
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003233 issue = None
3234 if options.issue:
3235 try:
3236 issue = int(options.issue)
3237 except ValueError:
3238 DieWithError('A review issue id is expected to be a number')
3239
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003240 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003241
3242 if options.comment:
3243 cl.AddComment(options.comment)
3244 return 0
3245
3246 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003247 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003248 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003249 summary.append({
3250 'date': message['date'],
3251 'lgtm': False,
3252 'message': message['text'],
3253 'not_lgtm': False,
3254 'sender': message['sender'],
3255 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003256 if message['disapproval']:
3257 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003258 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003259 elif message['approval']:
3260 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003261 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003262 elif message['sender'] == data['owner_email']:
3263 color = Fore.MAGENTA
3264 else:
3265 color = Fore.BLUE
3266 print '\n%s%s %s%s' % (
3267 color, message['date'].split('.', 1)[0], message['sender'],
3268 Fore.RESET)
3269 if message['text'].strip():
3270 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003271 if options.json_file:
3272 with open(options.json_file, 'wb') as f:
3273 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003274 return 0
3275
3276
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003277@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003278def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003279 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003280 parser.add_option('-d', '--display', action='store_true',
3281 help='Display the description instead of opening an editor')
martiniss@chromium.orgb73176a2016-04-29 17:13:55 +00003282 parser.add_option('-n', '--new-description',
3283 help='New description to set for this issue (- for stdin)')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003284
3285 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003286 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003287 options, args = parser.parse_args(args)
3288 _process_codereview_select_options(parser, options)
3289
3290 target_issue = None
3291 if len(args) > 0:
3292 issue_arg = ParseIssueNumberArgument(args[0])
3293 if not issue_arg.valid:
3294 parser.print_help()
3295 return 1
3296 target_issue = issue_arg.issue
3297
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003298 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003299
3300 cl = Changelist(
3301 auth_config=auth_config, issue=target_issue,
3302 codereview=options.forced_codereview)
3303
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003304 if not cl.GetIssue():
3305 DieWithError('This branch has no associated changelist.')
3306 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgb73176a2016-04-29 17:13:55 +00003307
smut@google.com34fb6b12015-07-13 20:03:26 +00003308 if options.display:
tandrii@chromium.org8c3b4422016-04-27 13:11:18 +00003309 print description.description
smut@google.com34fb6b12015-07-13 20:03:26 +00003310 return 0
martiniss@chromium.orgb73176a2016-04-29 17:13:55 +00003311
3312 if options.new_description:
3313 text = options.new_description
3314 if text == '-':
3315 text = '\n'.join(sys.stdin.splitlines())
3316
3317 description.set_description(text)
3318 else:
3319 description.prompt()
3320
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003321 if cl.GetDescription() != description.description:
3322 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003323 return 0
3324
3325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003326def CreateDescriptionFromLog(args):
3327 """Pulls out the commit log to use as a base for the CL description."""
3328 log_args = []
3329 if len(args) == 1 and not args[0].endswith('.'):
3330 log_args = [args[0] + '..']
3331 elif len(args) == 1 and args[0].endswith('...'):
3332 log_args = [args[0][:-1]]
3333 elif len(args) == 2:
3334 log_args = [args[0] + '..' + args[1]]
3335 else:
3336 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003337 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003338
3339
thestig@chromium.org44202a22014-03-11 19:22:18 +00003340def CMDlint(parser, args):
3341 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003342 parser.add_option('--filter', action='append', metavar='-x,+y',
3343 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003344 auth.add_auth_options(parser)
3345 options, args = parser.parse_args(args)
3346 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003347
3348 # Access to a protected member _XX of a client class
3349 # pylint: disable=W0212
3350 try:
3351 import cpplint
3352 import cpplint_chromium
3353 except ImportError:
3354 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3355 return 1
3356
3357 # Change the current working directory before calling lint so that it
3358 # shows the correct base.
3359 previous_cwd = os.getcwd()
3360 os.chdir(settings.GetRoot())
3361 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003362 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003363 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3364 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003365 if not files:
3366 print "Cannot lint an empty CL"
3367 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003368
3369 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003370 command = args + files
3371 if options.filter:
3372 command = ['--filter=' + ','.join(options.filter)] + command
3373 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003374
3375 white_regex = re.compile(settings.GetLintRegex())
3376 black_regex = re.compile(settings.GetLintIgnoreRegex())
3377 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3378 for filename in filenames:
3379 if white_regex.match(filename):
3380 if black_regex.match(filename):
3381 print "Ignoring file %s" % filename
3382 else:
3383 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3384 extra_check_functions)
3385 else:
3386 print "Skipping file %s" % filename
3387 finally:
3388 os.chdir(previous_cwd)
3389 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3390 if cpplint._cpplint_state.error_count != 0:
3391 return 1
3392 return 0
3393
3394
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003395def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003396 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003397 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003399 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003400 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003401 auth.add_auth_options(parser)
3402 options, args = parser.parse_args(args)
3403 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003404
sbc@chromium.org71437c02015-04-09 19:29:40 +00003405 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003406 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003407 return 1
3408
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003409 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003410 if args:
3411 base_branch = args[0]
3412 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003413 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003414 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003416 cl.RunHook(
3417 committing=not options.upload,
3418 may_prompt=False,
3419 verbose=options.verbose,
3420 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003421 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003422
3423
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003424def GenerateGerritChangeId(message):
3425 """Returns Ixxxxxx...xxx change id.
3426
3427 Works the same way as
3428 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3429 but can be called on demand on all platforms.
3430
3431 The basic idea is to generate git hash of a state of the tree, original commit
3432 message, author/committer info and timestamps.
3433 """
3434 lines = []
3435 tree_hash = RunGitSilent(['write-tree'])
3436 lines.append('tree %s' % tree_hash.strip())
3437 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3438 if code == 0:
3439 lines.append('parent %s' % parent.strip())
3440 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3441 lines.append('author %s' % author.strip())
3442 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3443 lines.append('committer %s' % committer.strip())
3444 lines.append('')
3445 # Note: Gerrit's commit-hook actually cleans message of some lines and
3446 # whitespace. This code is not doing this, but it clearly won't decrease
3447 # entropy.
3448 lines.append(message)
3449 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3450 stdin='\n'.join(lines))
3451 return 'I%s' % change_hash.strip()
3452
3453
wittman@chromium.org455dc922015-01-26 20:15:50 +00003454def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3455 """Computes the remote branch ref to use for the CL.
3456
3457 Args:
3458 remote (str): The git remote for the CL.
3459 remote_branch (str): The git remote branch for the CL.
3460 target_branch (str): The target branch specified by the user.
3461 pending_prefix (str): The pending prefix from the settings.
3462 """
3463 if not (remote and remote_branch):
3464 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003465
wittman@chromium.org455dc922015-01-26 20:15:50 +00003466 if target_branch:
3467 # Cannonicalize branch references to the equivalent local full symbolic
3468 # refs, which are then translated into the remote full symbolic refs
3469 # below.
3470 if '/' not in target_branch:
3471 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3472 else:
3473 prefix_replacements = (
3474 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3475 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3476 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3477 )
3478 match = None
3479 for regex, replacement in prefix_replacements:
3480 match = re.search(regex, target_branch)
3481 if match:
3482 remote_branch = target_branch.replace(match.group(0), replacement)
3483 break
3484 if not match:
3485 # This is a branch path but not one we recognize; use as-is.
3486 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003487 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3488 # Handle the refs that need to land in different refs.
3489 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003490
wittman@chromium.org455dc922015-01-26 20:15:50 +00003491 # Create the true path to the remote branch.
3492 # Does the following translation:
3493 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3494 # * refs/remotes/origin/master -> refs/heads/master
3495 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3496 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3497 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3498 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3499 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3500 'refs/heads/')
3501 elif remote_branch.startswith('refs/remotes/branch-heads'):
3502 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3503 # If a pending prefix exists then replace refs/ with it.
3504 if pending_prefix:
3505 remote_branch = remote_branch.replace('refs/', pending_prefix)
3506 return remote_branch
3507
3508
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003509def cleanup_list(l):
3510 """Fixes a list so that comma separated items are put as individual items.
3511
3512 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3513 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3514 """
3515 items = sum((i.split(',') for i in l), [])
3516 stripped_items = (i.strip() for i in items)
3517 return sorted(filter(None, stripped_items))
3518
3519
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003520@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003521def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003522 """Uploads the current changelist to codereview.
3523
3524 Can skip dependency patchset uploads for a branch by running:
3525 git config branch.branch_name.skip-deps-uploads True
3526 To unset run:
3527 git config --unset branch.branch_name.skip-deps-uploads
3528 Can also set the above globally by using the --global flag.
3529 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003530 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3531 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003532 parser.add_option('--bypass-watchlists', action='store_true',
3533 dest='bypass_watchlists',
3534 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003535 parser.add_option('-f', action='store_true', dest='force',
3536 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003537 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003538 parser.add_option('-t', dest='title',
3539 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003540 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003541 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003542 help='reviewer email addresses')
3543 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003544 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003545 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003546 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003547 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003548 parser.add_option('--emulate_svn_auto_props',
3549 '--emulate-svn-auto-props',
3550 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003551 dest="emulate_svn_auto_props",
3552 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003553 parser.add_option('-c', '--use-commit-queue', action='store_true',
3554 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003555 parser.add_option('--private', action='store_true',
3556 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003557 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003558 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003559 metavar='TARGET',
3560 help='Apply CL to remote ref TARGET. ' +
3561 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003562 parser.add_option('--squash', action='store_true',
3563 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003564 parser.add_option('--no-squash', action='store_true',
3565 help='Don\'t squash multiple commits into one ' +
3566 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003567 parser.add_option('--email', default=None,
3568 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003569 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3570 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003571 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3572 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003573 help='Send the patchset to do a CQ dry run right after '
3574 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003575 parser.add_option('--dependencies', action='store_true',
3576 help='Uploads CLs of all the local branches that depend on '
3577 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003578
rmistry@google.com2dd99862015-06-22 12:22:18 +00003579 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003580 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003581 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003582 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003583 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003584 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003585 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003586
sbc@chromium.org71437c02015-04-09 19:29:40 +00003587 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003588 return 1
3589
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003590 options.reviewers = cleanup_list(options.reviewers)
3591 options.cc = cleanup_list(options.cc)
3592
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003593 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3594 settings.GetIsGerrit()
3595
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003596 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003597 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003598
3599
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003600def IsSubmoduleMergeCommit(ref):
3601 # When submodules are added to the repo, we expect there to be a single
3602 # non-git-svn merge commit at remote HEAD with a signature comment.
3603 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003604 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003605 return RunGit(cmd) != ''
3606
3607
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003608def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003609 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003610
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003611 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3612 upstream and closes the issue automatically and atomically.
3613
3614 Otherwise (in case of Rietveld):
3615 Squashes branch into a single commit.
3616 Updates changelog with metadata (e.g. pointer to review).
3617 Pushes/dcommits the code upstream.
3618 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619 """
3620 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3621 help='bypass upload presubmit hook')
3622 parser.add_option('-m', dest='message',
3623 help="override review description")
3624 parser.add_option('-f', action='store_true', dest='force',
3625 help="force yes to questions (don't prompt)")
3626 parser.add_option('-c', dest='contributor',
3627 help="external contributor for patch (appended to " +
3628 "description and used as author for git). Should be " +
3629 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003630 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003631 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003632 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003633 auth_config = auth.extract_auth_config_from_options(options)
3634
3635 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003636
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003637 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3638 if cl.IsGerrit():
3639 if options.message:
3640 # This could be implemented, but it requires sending a new patch to
3641 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3642 # Besides, Gerrit has the ability to change the commit message on submit
3643 # automatically, thus there is no need to support this option (so far?).
3644 parser.error('-m MESSAGE option is not supported for Gerrit.')
3645 if options.contributor:
3646 parser.error(
3647 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3648 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3649 'the contributor\'s "name <email>". If you can\'t upload such a '
3650 'commit for review, contact your repository admin and request'
3651 '"Forge-Author" permission.')
3652 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3653 options.verbose)
3654
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003655 current = cl.GetBranch()
3656 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3657 if not settings.GetIsGitSvn() and remote == '.':
3658 print
3659 print 'Attempting to push branch %r into another local branch!' % current
3660 print
3661 print 'Either reparent this branch on top of origin/master:'
3662 print ' git reparent-branch --root'
3663 print
3664 print 'OR run `git rebase-update` if you think the parent branch is already'
3665 print 'committed.'
3666 print
3667 print ' Current parent: %r' % upstream_branch
3668 return 1
3669
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003670 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003671 # Default to merging against our best guess of the upstream branch.
3672 args = [cl.GetUpstreamBranch()]
3673
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003674 if options.contributor:
3675 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3676 print "Please provide contibutor as 'First Last <email@example.com>'"
3677 return 1
3678
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003679 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003680 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003681
sbc@chromium.org71437c02015-04-09 19:29:40 +00003682 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003683 return 1
3684
3685 # This rev-list syntax means "show all commits not in my branch that
3686 # are in base_branch".
3687 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3688 base_branch]).splitlines()
3689 if upstream_commits:
3690 print ('Base branch "%s" has %d commits '
3691 'not in this branch.' % (base_branch, len(upstream_commits)))
3692 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3693 return 1
3694
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003695 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003696 svn_head = None
3697 if cmd == 'dcommit' or base_has_submodules:
3698 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3699 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003700
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003701 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003702 # If the base_head is a submodule merge commit, the first parent of the
3703 # base_head should be a git-svn commit, which is what we're interested in.
3704 base_svn_head = base_branch
3705 if base_has_submodules:
3706 base_svn_head += '^1'
3707
3708 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709 if extra_commits:
3710 print ('This branch has %d additional commits not upstreamed yet.'
3711 % len(extra_commits.splitlines()))
3712 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3713 'before attempting to %s.' % (base_branch, cmd))
3714 return 1
3715
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003716 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003717 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003718 author = None
3719 if options.contributor:
3720 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003721 hook_results = cl.RunHook(
3722 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003723 may_prompt=not options.force,
3724 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003725 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003726 if not hook_results.should_continue():
3727 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003728
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003729 # Check the tree status if the tree status URL is set.
3730 status = GetTreeStatus()
3731 if 'closed' == status:
3732 print('The tree is closed. Please wait for it to reopen. Use '
3733 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3734 return 1
3735 elif 'unknown' == status:
3736 print('Unable to determine tree status. Please verify manually and '
3737 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3738 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003739
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003740 change_desc = ChangeDescription(options.message)
3741 if not change_desc.description and cl.GetIssue():
3742 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003743
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003744 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003745 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003746 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003747 else:
3748 print 'No description set.'
3749 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3750 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003751
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003752 # Keep a separate copy for the commit message, because the commit message
3753 # contains the link to the Rietveld issue, while the Rietveld message contains
3754 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003755 # Keep a separate copy for the commit message.
3756 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003757 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003758
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003759 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003760 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003761 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003762 # after it. Add a period on a new line to circumvent this. Also add a space
3763 # before the period to make sure that Gitiles continues to correctly resolve
3764 # the URL.
3765 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003766 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003767 commit_desc.append_footer('Patch from %s.' % options.contributor)
3768
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003769 print('Description:')
3770 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003771
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003772 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003773 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003774 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003775
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003776 # We want to squash all this branch's commits into one commit with the proper
3777 # description. We do this by doing a "reset --soft" to the base branch (which
3778 # keeps the working copy the same), then dcommitting that. If origin/master
3779 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3780 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003781 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003782 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3783 # Delete the branches if they exist.
3784 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3785 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3786 result = RunGitWithCode(showref_cmd)
3787 if result[0] == 0:
3788 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003789
3790 # We might be in a directory that's present in this branch but not in the
3791 # trunk. Move up to the top of the tree so that git commands that expect a
3792 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003793 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003794 if rel_base_path:
3795 os.chdir(rel_base_path)
3796
3797 # Stuff our change into the merge branch.
3798 # We wrap in a try...finally block so if anything goes wrong,
3799 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003800 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003801 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003802 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003803 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003804 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003805 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003806 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003807 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003808 RunGit(
3809 [
3810 'commit', '--author', options.contributor,
3811 '-m', commit_desc.description,
3812 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003814 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003815 if base_has_submodules:
3816 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3817 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3818 RunGit(['checkout', CHERRY_PICK_BRANCH])
3819 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003820 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003821 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003822 mirror = settings.GetGitMirror(remote)
3823 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003824 pending_prefix = settings.GetPendingRefPrefix()
3825 if not pending_prefix or branch.startswith(pending_prefix):
3826 # If not using refs/pending/heads/* at all, or target ref is already set
3827 # to pending, then push to the target ref directly.
3828 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003829 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003830 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003831 else:
3832 # Cherry-pick the change on top of pending ref and then push it.
3833 assert branch.startswith('refs/'), branch
3834 assert pending_prefix[-1] == '/', pending_prefix
3835 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003836 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003837 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003838 if retcode == 0:
3839 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003840 else:
3841 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003842 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003843 'svn', 'dcommit',
3844 '-C%s' % options.similarity,
3845 '--no-rebase', '--rmdir',
3846 ]
3847 if settings.GetForceHttpsCommitUrl():
3848 # Allow forcing https commit URLs for some projects that don't allow
3849 # committing to http URLs (like Google Code).
3850 remote_url = cl.GetGitSvnRemoteUrl()
3851 if urlparse.urlparse(remote_url).scheme == 'http':
3852 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003853 cmd_args.append('--commit-url=%s' % remote_url)
3854 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003855 if 'Committed r' in output:
3856 revision = re.match(
3857 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3858 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003859 finally:
3860 # And then swap back to the original branch and clean up.
3861 RunGit(['checkout', '-q', cl.GetBranch()])
3862 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003863 if base_has_submodules:
3864 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003865
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003866 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003867 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003868 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003869
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003870 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003871 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003872 try:
3873 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3874 # We set pushed_to_pending to False, since it made it all the way to the
3875 # real ref.
3876 pushed_to_pending = False
3877 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003878 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003879
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003880 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003881 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003883 if not to_pending:
3884 if viewvc_url and revision:
3885 change_desc.append_footer(
3886 'Committed: %s%s' % (viewvc_url, revision))
3887 elif revision:
3888 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003889 print ('Closing issue '
3890 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003891 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003893 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003894 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003895 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003896 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003897 if options.bypass_hooks:
3898 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3899 else:
3900 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003901 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003902 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003903
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003904 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003905 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3906 print 'The commit is in the pending queue (%s).' % pending_ref
3907 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003908 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003909 'footer.' % branch)
3910
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003911 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3912 if os.path.isfile(hook):
3913 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003914
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003915 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003916
3917
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003918def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3919 print
3920 print 'Waiting for commit to be landed on %s...' % real_ref
3921 print '(If you are impatient, you may Ctrl-C once without harm)'
3922 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3923 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003924 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003925
3926 loop = 0
3927 while True:
3928 sys.stdout.write('fetching (%d)... \r' % loop)
3929 sys.stdout.flush()
3930 loop += 1
3931
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003932 if mirror:
3933 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003934 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3935 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3936 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3937 for commit in commits.splitlines():
3938 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3939 print 'Found commit on %s' % real_ref
3940 return commit
3941
3942 current_rev = to_rev
3943
3944
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003945def PushToGitPending(remote, pending_ref, upstream_ref):
3946 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3947
3948 Returns:
3949 (retcode of last operation, output log of last operation).
3950 """
3951 assert pending_ref.startswith('refs/'), pending_ref
3952 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3953 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3954 code = 0
3955 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003956 max_attempts = 3
3957 attempts_left = max_attempts
3958 while attempts_left:
3959 if attempts_left != max_attempts:
3960 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3961 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003962
3963 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003964 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003965 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003966 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003967 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003968 print 'Fetch failed with exit code %d.' % code
3969 if out.strip():
3970 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003971 continue
3972
3973 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003974 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003975 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003976 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003977 if code:
3978 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003979 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3980 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003981 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3982 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003983 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003984 return code, out
3985
3986 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003987 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003988 code, out = RunGitWithCode(
3989 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3990 if code == 0:
3991 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003992 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003993 return code, out
3994
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003995 print 'Push failed with exit code %d.' % code
3996 if out.strip():
3997 print out.strip()
3998 if IsFatalPushFailure(out):
3999 print (
4000 'Fatal push error. Make sure your .netrc credentials and git '
4001 'user.email are correct and you have push access to the repo.')
4002 return code, out
4003
4004 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004005 return code, out
4006
4007
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004008def IsFatalPushFailure(push_stdout):
4009 """True if retrying push won't help."""
4010 return '(prohibited by Gerrit)' in push_stdout
4011
4012
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004013@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004014def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004015 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004016 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004017 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004018 # If it looks like previous commits were mirrored with git-svn.
4019 message = """This repository appears to be a git-svn mirror, but no
4020upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4021 else:
4022 message = """This doesn't appear to be an SVN repository.
4023If your project has a true, writeable git repository, you probably want to run
4024'git cl land' instead.
4025If your project has a git mirror of an upstream SVN master, you probably need
4026to run 'git svn init'.
4027
4028Using the wrong command might cause your commit to appear to succeed, and the
4029review to be closed, without actually landing upstream. If you choose to
4030proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004031 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004032 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004033 return SendUpstream(parser, args, 'dcommit')
4034
4035
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004036@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004037def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004038 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004039 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004040 print('This appears to be an SVN repository.')
4041 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004042 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004043 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004044 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004045
4046
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004047@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004049 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050 parser.add_option('-b', dest='newbranch',
4051 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004052 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004054 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4055 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004056 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004057 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004058 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004059 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004060 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004061 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004062
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004063
4064 group = optparse.OptionGroup(
4065 parser,
4066 'Options for continuing work on the current issue uploaded from a '
4067 'different clone (e.g. different machine). Must be used independently '
4068 'from the other options. No issue number should be specified, and the '
4069 'branch must have an issue number associated with it')
4070 group.add_option('--reapply', action='store_true', dest='reapply',
4071 help='Reset the branch and reapply the issue.\n'
4072 'CAUTION: This will undo any local changes in this '
4073 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004074
4075 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004076 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004077 parser.add_option_group(group)
4078
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004079 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004080 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004081 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004082 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004083 auth_config = auth.extract_auth_config_from_options(options)
4084
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004085 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004086
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004087 issue_arg = None
4088 if options.reapply :
4089 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004090 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004091
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004092 issue_arg = cl.GetIssue()
4093 upstream = cl.GetUpstreamBranch()
4094 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004095 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004096
4097 RunGit(['reset', '--hard', upstream])
4098 if options.pull:
4099 RunGit(['pull'])
4100 else:
4101 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004102 parser.error('Must specify issue number or url')
4103 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004104
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004105 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004106 parser.print_help()
4107 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004108
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004109 if cl.IsGerrit():
4110 if options.reject:
4111 parser.error('--reject is not supported with Gerrit codereview.')
4112 if options.nocommit:
4113 parser.error('--nocommit is not supported with Gerrit codereview.')
4114 if options.directory:
4115 parser.error('--directory is not supported with Gerrit codereview.')
4116
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004117 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004118 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004119 return 1
4120
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004121 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004122 if options.reapply:
4123 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004124 if options.force:
4125 RunGit(['branch', '-D', options.newbranch],
4126 stderr=subprocess2.PIPE, error_ok=True)
4127 RunGit(['checkout', '-b', options.newbranch,
4128 Changelist().GetUpstreamBranch()])
4129
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004130 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
4131 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004132
4133
4134def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004135 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004136 # Provide a wrapper for git svn rebase to help avoid accidental
4137 # git svn dcommit.
4138 # It's the only command that doesn't use parser at all since we just defer
4139 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004140
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004141 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004142
4143
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004144def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004145 """Fetches the tree status and returns either 'open', 'closed',
4146 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004147 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004148 if url:
4149 status = urllib2.urlopen(url).read().lower()
4150 if status.find('closed') != -1 or status == '0':
4151 return 'closed'
4152 elif status.find('open') != -1 or status == '1':
4153 return 'open'
4154 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004155 return 'unset'
4156
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004157
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004158def GetTreeStatusReason():
4159 """Fetches the tree status from a json url and returns the message
4160 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004161 url = settings.GetTreeStatusUrl()
4162 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004163 connection = urllib2.urlopen(json_url)
4164 status = json.loads(connection.read())
4165 connection.close()
4166 return status['message']
4167
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004168
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004169def GetBuilderMaster(bot_list):
4170 """For a given builder, fetch the master from AE if available."""
4171 map_url = 'https://builders-map.appspot.com/'
4172 try:
4173 master_map = json.load(urllib2.urlopen(map_url))
4174 except urllib2.URLError as e:
4175 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4176 (map_url, e))
4177 except ValueError as e:
4178 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4179 if not master_map:
4180 return None, 'Failed to build master map.'
4181
4182 result_master = ''
4183 for bot in bot_list:
4184 builder = bot.split(':', 1)[0]
4185 master_list = master_map.get(builder, [])
4186 if not master_list:
4187 return None, ('No matching master for builder %s.' % builder)
4188 elif len(master_list) > 1:
4189 return None, ('The builder name %s exists in multiple masters %s.' %
4190 (builder, master_list))
4191 else:
4192 cur_master = master_list[0]
4193 if not result_master:
4194 result_master = cur_master
4195 elif result_master != cur_master:
4196 return None, 'The builders do not belong to the same master.'
4197 return result_master, None
4198
4199
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004200def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004201 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004202 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004203 status = GetTreeStatus()
4204 if 'unset' == status:
4205 print 'You must configure your tree status URL by running "git cl config".'
4206 return 2
4207
4208 print "The tree is %s" % status
4209 print
4210 print GetTreeStatusReason()
4211 if status != 'open':
4212 return 1
4213 return 0
4214
4215
maruel@chromium.org15192402012-09-06 12:38:29 +00004216def CMDtry(parser, args):
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004217 """Triggers try jobs through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004218 group = optparse.OptionGroup(parser, "Try job options")
4219 group.add_option(
4220 "-b", "--bot", action="append",
4221 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4222 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004223 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004224 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004225 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004226 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004227 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004228 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004229 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004230 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004231 "-r", "--revision",
4232 help="Revision to use for the try job; default: the "
4233 "revision will be determined by the try server; see "
4234 "its waterfall for more info")
4235 group.add_option(
4236 "-c", "--clobber", action="store_true", default=False,
4237 help="Force a clobber before building; e.g. don't do an "
4238 "incremental build")
4239 group.add_option(
4240 "--project",
4241 help="Override which project to use. Projects are defined "
4242 "server-side to define what default bot set to use")
4243 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004244 "-p", "--property", dest="properties", action="append", default=[],
4245 help="Specify generic properties in the form -p key1=value1 -p "
4246 "key2=value2 etc (buildbucket only). The value will be treated as "
4247 "json if decodable, or as string otherwise.")
4248 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004249 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004250 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004251 "--use-rietveld", action="store_true", default=False,
4252 help="Use Rietveld to trigger try jobs.")
4253 group.add_option(
4254 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4255 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004256 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004257 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004258 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004259 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004260
machenbach@chromium.org45453142015-09-15 08:45:22 +00004261 if options.use_rietveld and options.properties:
4262 parser.error('Properties can only be specified with buildbucket')
4263
4264 # Make sure that all properties are prop=value pairs.
4265 bad_params = [x for x in options.properties if '=' not in x]
4266 if bad_params:
4267 parser.error('Got properties with missing "=": %s' % bad_params)
4268
maruel@chromium.org15192402012-09-06 12:38:29 +00004269 if args:
4270 parser.error('Unknown arguments: %s' % args)
4271
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004272 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004273 if not cl.GetIssue():
4274 parser.error('Need to upload first')
4275
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004276 if cl.IsGerrit():
4277 parser.error(
4278 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4279 'If your project has Commit Queue, dry run is a workaround:\n'
4280 ' git cl set-commit --dry-run')
4281 # Code below assumes Rietveld issue.
4282 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4283
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004284 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004285 if props.get('closed'):
4286 parser.error('Cannot send tryjobs for a closed CL')
4287
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004288 if props.get('private'):
4289 parser.error('Cannot use trybots with private issue')
4290
maruel@chromium.org15192402012-09-06 12:38:29 +00004291 if not options.name:
4292 options.name = cl.GetBranch()
4293
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004294 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004295 options.master, err_msg = GetBuilderMaster(options.bot)
4296 if err_msg:
4297 parser.error('Tryserver master cannot be found because: %s\n'
4298 'Please manually specify the tryserver master'
4299 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004300
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004301 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004302 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004303 if not options.bot:
4304 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004305
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004306 # Get try masters from PRESUBMIT.py files.
4307 masters = presubmit_support.DoGetTryMasters(
4308 change,
4309 change.LocalPaths(),
4310 settings.GetRoot(),
4311 None,
4312 None,
4313 options.verbose,
4314 sys.stdout)
4315 if masters:
4316 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004317
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004318 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4319 options.bot = presubmit_support.DoGetTrySlaves(
4320 change,
4321 change.LocalPaths(),
4322 settings.GetRoot(),
4323 None,
4324 None,
4325 options.verbose,
4326 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004327
4328 if not options.bot:
4329 # Get try masters from cq.cfg if any.
4330 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4331 # location.
4332 cq_cfg = os.path.join(change.RepositoryRoot(),
4333 'infra', 'config', 'cq.cfg')
4334 if os.path.exists(cq_cfg):
4335 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004336 cq_masters = commit_queue.get_master_builder_map(
4337 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004338 for master, builders in cq_masters.iteritems():
4339 for builder in builders:
4340 # Skip presubmit builders, because these will fail without LGTM.
machenbach@chromium.org2403e802016-04-29 12:34:42 +00004341 masters.setdefault(master, {})[builder] = ['defaulttests']
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004342 if masters:
4343 return masters
4344
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004345 if not options.bot:
4346 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004347
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004348 builders_and_tests = {}
4349 # TODO(machenbach): The old style command-line options don't support
4350 # multiple try masters yet.
4351 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4352 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4353
4354 for bot in old_style:
4355 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004356 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004357 elif ',' in bot:
4358 parser.error('Specify one bot per --bot flag')
4359 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004360 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004361
4362 for bot, tests in new_style:
4363 builders_and_tests.setdefault(bot, []).extend(tests)
4364
4365 # Return a master map with one master to be backwards compatible. The
4366 # master name defaults to an empty string, which will cause the master
4367 # not to be set on rietveld (deprecated).
4368 return {options.master: builders_and_tests}
4369
4370 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004371
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004372 for builders in masters.itervalues():
4373 if any('triggered' in b for b in builders):
4374 print >> sys.stderr, (
4375 'ERROR You are trying to send a job to a triggered bot. This type of'
4376 ' bot requires an\ninitial job from a parent (usually a builder). '
4377 'Instead send your job to the parent.\n'
4378 'Bot list: %s' % builders)
4379 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004380
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004381 patchset = cl.GetMostRecentPatchset()
4382 if patchset and patchset != cl.GetPatchset():
4383 print(
4384 '\nWARNING Mismatch between local config and server. Did a previous '
4385 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4386 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004387 if options.luci:
4388 trigger_luci_job(cl, masters, options)
4389 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004390 try:
4391 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4392 except BuildbucketResponseException as ex:
4393 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004394 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004395 except Exception as e:
4396 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4397 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4398 e, stacktrace)
4399 return 1
4400 else:
4401 try:
4402 cl.RpcServer().trigger_distributed_try_jobs(
4403 cl.GetIssue(), patchset, options.name, options.clobber,
4404 options.revision, masters)
4405 except urllib2.HTTPError as e:
4406 if e.code == 404:
4407 print('404 from rietveld; '
4408 'did you mean to use "git try" instead of "git cl try"?')
4409 return 1
4410 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004411
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004412 for (master, builders) in sorted(masters.iteritems()):
4413 if master:
4414 print 'Master: %s' % master
4415 length = max(len(builder) for builder in builders)
4416 for builder in sorted(builders):
4417 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004418 return 0
4419
4420
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004421def CMDtry_results(parser, args):
4422 group = optparse.OptionGroup(parser, "Try job results options")
4423 group.add_option(
4424 "-p", "--patchset", type=int, help="patchset number if not current.")
4425 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004426 "--print-master", action='store_true', help="print master name as well.")
4427 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004428 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004429 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004430 group.add_option(
4431 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4432 help="Host of buildbucket. The default host is %default.")
4433 parser.add_option_group(group)
4434 auth.add_auth_options(parser)
4435 options, args = parser.parse_args(args)
4436 if args:
4437 parser.error('Unrecognized args: %s' % ' '.join(args))
4438
4439 auth_config = auth.extract_auth_config_from_options(options)
4440 cl = Changelist(auth_config=auth_config)
4441 if not cl.GetIssue():
4442 parser.error('Need to upload first')
4443
4444 if not options.patchset:
4445 options.patchset = cl.GetMostRecentPatchset()
4446 if options.patchset and options.patchset != cl.GetPatchset():
4447 print(
4448 '\nWARNING Mismatch between local config and server. Did a previous '
4449 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4450 'Continuing using\npatchset %s.\n' % options.patchset)
4451 try:
4452 jobs = fetch_try_jobs(auth_config, cl, options)
4453 except BuildbucketResponseException as ex:
4454 print 'Buildbucket error: %s' % ex
4455 return 1
4456 except Exception as e:
4457 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4458 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4459 e, stacktrace)
4460 return 1
4461 print_tryjobs(options, jobs)
4462 return 0
4463
4464
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004465@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004466def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004467 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004468 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004469 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004470 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004471
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004473 if args:
4474 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004475 branch = cl.GetBranch()
4476 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004477 cl = Changelist()
4478 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004479
4480 # Clear configured merge-base, if there is one.
4481 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004482 else:
4483 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004484 return 0
4485
4486
thestig@chromium.org00858c82013-12-02 23:08:03 +00004487def CMDweb(parser, args):
4488 """Opens the current CL in the web browser."""
4489 _, args = parser.parse_args(args)
4490 if args:
4491 parser.error('Unrecognized args: %s' % ' '.join(args))
4492
4493 issue_url = Changelist().GetIssueURL()
4494 if not issue_url:
4495 print >> sys.stderr, 'ERROR No issue to open'
4496 return 1
4497
4498 webbrowser.open(issue_url)
4499 return 0
4500
4501
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004502def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004503 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004504 parser.add_option('-d', '--dry-run', action='store_true',
4505 help='trigger in dry run mode')
4506 parser.add_option('-c', '--clear', action='store_true',
4507 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004508 auth.add_auth_options(parser)
4509 options, args = parser.parse_args(args)
4510 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004511 if args:
4512 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004513 if options.dry_run and options.clear:
4514 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4515
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004516 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004517 if options.clear:
4518 state = _CQState.CLEAR
4519 elif options.dry_run:
4520 state = _CQState.DRY_RUN
4521 else:
4522 state = _CQState.COMMIT
4523 if not cl.GetIssue():
4524 parser.error('Must upload the issue first')
4525 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004526 return 0
4527
4528
groby@chromium.org411034a2013-02-26 15:12:01 +00004529def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004530 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004531 auth.add_auth_options(parser)
4532 options, args = parser.parse_args(args)
4533 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004534 if args:
4535 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004536 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004537 # Ensure there actually is an issue to close.
4538 cl.GetDescription()
4539 cl.CloseIssue()
4540 return 0
4541
4542
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004543def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004544 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004545 auth.add_auth_options(parser)
4546 options, args = parser.parse_args(args)
4547 auth_config = auth.extract_auth_config_from_options(options)
4548 if args:
4549 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004550
4551 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004552 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004553 # Staged changes would be committed along with the patch from last
4554 # upload, hence counted toward the "last upload" side in the final
4555 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004556 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004557 return 1
4558
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004559 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004560 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004561 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004562 if not issue:
4563 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004564 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004565 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004566
4567 # Create a new branch based on the merge-base
4568 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004569 # Clear cached branch in cl object, to avoid overwriting original CL branch
4570 # properties.
4571 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004572 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004573 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004574 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004575 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004576 return rtn
4577
wychen@chromium.org06928532015-02-03 02:11:29 +00004578 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004579 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004580 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004581 finally:
4582 RunGit(['checkout', '-q', branch])
4583 RunGit(['branch', '-D', TMP_BRANCH])
4584
4585 return 0
4586
4587
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004588def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004589 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004590 parser.add_option(
4591 '--no-color',
4592 action='store_true',
4593 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004594 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004595 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004596 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004597
4598 author = RunGit(['config', 'user.email']).strip() or None
4599
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004600 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004601
4602 if args:
4603 if len(args) > 1:
4604 parser.error('Unknown args')
4605 base_branch = args[0]
4606 else:
4607 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004608 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004609
4610 change = cl.GetChange(base_branch, None)
4611 return owners_finder.OwnersFinder(
4612 [f.LocalPath() for f in
4613 cl.GetChange(base_branch, None).AffectedFiles()],
4614 change.RepositoryRoot(), author,
4615 fopen=file, os_path=os.path, glob=glob.glob,
4616 disable_color=options.no_color).run()
4617
4618
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004619def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004620 """Generates a diff command."""
4621 # Generate diff for the current branch's changes.
4622 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4623 upstream_commit, '--' ]
4624
4625 if args:
4626 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004627 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004628 diff_cmd.append(arg)
4629 else:
4630 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004631
4632 return diff_cmd
4633
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004634def MatchingFileType(file_name, extensions):
4635 """Returns true if the file name ends with one of the given extensions."""
4636 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004637
enne@chromium.org555cfe42014-01-29 18:21:39 +00004638@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004639def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004640 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004641 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004642 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004643 parser.add_option('--full', action='store_true',
4644 help='Reformat the full content of all touched files')
4645 parser.add_option('--dry-run', action='store_true',
4646 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004647 parser.add_option('--python', action='store_true',
4648 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004649 parser.add_option('--diff', action='store_true',
4650 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004651 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004652
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004653 # git diff generates paths against the root of the repository. Change
4654 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004655 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004656 if rel_base_path:
4657 os.chdir(rel_base_path)
4658
digit@chromium.org29e47272013-05-17 17:01:46 +00004659 # Grab the merge-base commit, i.e. the upstream commit of the current
4660 # branch when it was created or the last time it was rebased. This is
4661 # to cover the case where the user may have called "git fetch origin",
4662 # moving the origin branch to a newer commit, but hasn't rebased yet.
4663 upstream_commit = None
4664 cl = Changelist()
4665 upstream_branch = cl.GetUpstreamBranch()
4666 if upstream_branch:
4667 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4668 upstream_commit = upstream_commit.strip()
4669
4670 if not upstream_commit:
4671 DieWithError('Could not find base commit for this branch. '
4672 'Are you in detached state?')
4673
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004674 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4675 diff_output = RunGit(changed_files_cmd)
4676 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004677 # Filter out files deleted by this CL
4678 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004679
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004680 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4681 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4682 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004683 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004684
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004685 top_dir = os.path.normpath(
4686 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4687
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004688 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4689 # formatted. This is used to block during the presubmit.
4690 return_value = 0
4691
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004692 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004693 # Locate the clang-format binary in the checkout
4694 try:
4695 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4696 except clang_format.NotFoundError, e:
4697 DieWithError(e)
4698
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004699 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004700 cmd = [clang_format_tool]
4701 if not opts.dry_run and not opts.diff:
4702 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004703 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004704 if opts.diff:
4705 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004706 else:
4707 env = os.environ.copy()
4708 env['PATH'] = str(os.path.dirname(clang_format_tool))
4709 try:
4710 script = clang_format.FindClangFormatScriptInChromiumTree(
4711 'clang-format-diff.py')
4712 except clang_format.NotFoundError, e:
4713 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004714
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004715 cmd = [sys.executable, script, '-p0']
4716 if not opts.dry_run and not opts.diff:
4717 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004718
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004719 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4720 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004721
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004722 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4723 if opts.diff:
4724 sys.stdout.write(stdout)
4725 if opts.dry_run and len(stdout) > 0:
4726 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004727
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004728 # Similar code to above, but using yapf on .py files rather than clang-format
4729 # on C/C++ files
4730 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004731 yapf_tool = gclient_utils.FindExecutable('yapf')
4732 if yapf_tool is None:
4733 DieWithError('yapf not found in PATH')
4734
4735 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004736 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004737 cmd = [yapf_tool]
4738 if not opts.dry_run and not opts.diff:
4739 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004740 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004741 if opts.diff:
4742 sys.stdout.write(stdout)
4743 else:
4744 # TODO(sbc): yapf --lines mode still has some issues.
4745 # https://github.com/google/yapf/issues/154
4746 DieWithError('--python currently only works with --full')
4747
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004748 # Dart's formatter does not have the nice property of only operating on
4749 # modified chunks, so hard code full.
4750 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004751 try:
4752 command = [dart_format.FindDartFmtToolInChromiumTree()]
4753 if not opts.dry_run and not opts.diff:
4754 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004755 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004756
ppi@chromium.org6593d932016-03-03 15:41:15 +00004757 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004758 if opts.dry_run and stdout:
4759 return_value = 2
4760 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004761 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4762 'found in this checkout. Files in other languages are still ' +
4763 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004764
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004765 # Format GN build files. Always run on full build files for canonical form.
4766 if gn_diff_files:
4767 cmd = ['gn', 'format']
4768 if not opts.dry_run and not opts.diff:
4769 cmd.append('--in-place')
4770 for gn_diff_file in gn_diff_files:
bsep@chromium.org627d9002016-04-29 00:00:52 +00004771 stdout = RunCommand(cmd + [gn_diff_file],
4772 shell=sys.platform == 'win32',
4773 cwd=top_dir)
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004774 if opts.diff:
4775 sys.stdout.write(stdout)
4776
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004777 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004778
4779
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004780@subcommand.usage('<codereview url or issue id>')
4781def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004782 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004783 _, args = parser.parse_args(args)
4784
4785 if len(args) != 1:
4786 parser.print_help()
4787 return 1
4788
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004789 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004790 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004791 parser.print_help()
4792 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004793 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004794
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004795 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004796 output = RunGit(['config', '--local', '--get-regexp',
4797 r'branch\..*\.%s' % issueprefix],
4798 error_ok=True)
4799 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004800 if issue == target_issue:
4801 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004802
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004803 branches = []
4804 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004805 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004806 if len(branches) == 0:
4807 print 'No branch found for issue %s.' % target_issue
4808 return 1
4809 if len(branches) == 1:
4810 RunGit(['checkout', branches[0]])
4811 else:
4812 print 'Multiple branches match issue %s:' % target_issue
4813 for i in range(len(branches)):
4814 print '%d: %s' % (i, branches[i])
4815 which = raw_input('Choose by index: ')
4816 try:
4817 RunGit(['checkout', branches[int(which)]])
4818 except (IndexError, ValueError):
4819 print 'Invalid selection, not checking out any branch.'
4820 return 1
4821
4822 return 0
4823
4824
maruel@chromium.org29404b52014-09-08 22:58:00 +00004825def CMDlol(parser, args):
4826 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004827 print zlib.decompress(base64.b64decode(
4828 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4829 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4830 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4831 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004832 return 0
4833
4834
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004835class OptionParser(optparse.OptionParser):
4836 """Creates the option parse and add --verbose support."""
4837 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004838 optparse.OptionParser.__init__(
4839 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004840 self.add_option(
4841 '-v', '--verbose', action='count', default=0,
4842 help='Use 2 times for more debugging info')
4843
4844 def parse_args(self, args=None, values=None):
4845 options, args = optparse.OptionParser.parse_args(self, args, values)
4846 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4847 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4848 return options, args
4849
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004850
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004851def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004852 if sys.hexversion < 0x02060000:
4853 print >> sys.stderr, (
4854 '\nYour python version %s is unsupported, please upgrade.\n' %
4855 sys.version.split(' ', 1)[0])
4856 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004857
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004858 # Reload settings.
4859 global settings
4860 settings = Settings()
4861
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004862 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004863 dispatcher = subcommand.CommandDispatcher(__name__)
4864 try:
4865 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004866 except auth.AuthenticationError as e:
4867 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004868 except urllib2.HTTPError, e:
4869 if e.code != 500:
4870 raise
4871 DieWithError(
4872 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4873 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004874 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004875
4876
4877if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004878 # These affect sys.stdout so do it outside of main() to simplify mocks in
4879 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004880 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004881 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004882 try:
4883 sys.exit(main(sys.argv[1:]))
4884 except KeyboardInterrupt:
4885 sys.stderr.write('interrupted\n')
4886 sys.exit(1)