blob: 05fb354d1ea11c47bdcf831eddf107324543854c [file] [log] [blame]
[email protected]405b87e2015-11-12 18:08:341#!/usr/bin/env python
[email protected]183df1a2012-01-04 19:44:552# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]725f1c32011-04-01 20:24:543# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
[email protected]cc51cd02010-12-23 00:48:396# Copyright (C) 2008 Evan Martin <[email protected]>
7
[email protected]aa5ced12016-03-29 09:41:148"""A git-command for integrating reviews on Rietveld and Gerrit."""
[email protected]725f1c32011-04-01 20:24:549
[email protected]6a0b07c2013-07-10 01:29:1910from distutils.version import LooseVersion
[email protected]ffde55c2015-03-12 00:44:1711from multiprocessing.pool import ThreadPool
[email protected]3421c992014-11-02 02:20:3212import base64
[email protected]2dd99862015-06-22 12:22:1813import collections
[email protected]faf3fdf2013-09-20 02:11:4814import glob
[email protected]6ebaf782015-05-12 19:17:5415import httplib
[email protected]4f6852c2012-04-20 20:39:2016import json
[email protected]cc51cd02010-12-23 00:48:3917import logging
[email protected]cf197482016-04-29 20:15:5318import multiprocessing
[email protected]cc51cd02010-12-23 00:48:3919import optparse
20import os
[email protected]cc51cd02010-12-23 00:48:3921import re
[email protected]78c4b982012-02-14 02:20:2622import stat
[email protected]cc51cd02010-12-23 00:48:3923import sys
[email protected]cc51cd02010-12-23 00:48:3924import textwrap
[email protected]6ebaf782015-05-12 19:17:5425import time
26import traceback
[email protected]b015fac2016-02-26 14:52:0127import urllib
[email protected]cc51cd02010-12-23 00:48:3928import urllib2
[email protected]967c0a82013-06-17 22:52:2429import urlparse
[email protected]b015fac2016-02-26 14:52:0130import uuid
[email protected]00858c82013-12-02 23:08:0331import webbrowser
[email protected]3421c992014-11-02 02:20:3232import zlib
[email protected]cc51cd02010-12-23 00:48:3933
34try:
[email protected]c98c0c52011-04-06 13:39:4335 import readline # pylint: disable=F0401,W0611
[email protected]cc51cd02010-12-23 00:48:3936except ImportError:
37 pass
38
[email protected]2e23ce32013-05-07 12:42:2839from third_party import colorama
[email protected]6ebaf782015-05-12 19:17:5440from third_party import httplib2
[email protected]2a74d372011-03-29 19:05:5041from third_party import upload
[email protected]cf6a5d22015-04-09 22:02:0042import auth
[email protected]feb9e2a2015-09-25 19:11:0943from luci_hacks import trigger_luci_job as luci_trigger
[email protected]3ac1c4e2014-01-16 02:44:4244import clang_format
[email protected]71184c02016-01-13 15:18:4445import commit_queue
[email protected]e0a7c5d2015-02-23 20:30:0846import dart_format
[email protected]596cd5c2016-04-04 21:34:3947import setup_color
[email protected]6f09cd92011-04-01 16:38:1248import fix_encoding
[email protected]0e0436a2011-10-25 13:32:4149import gclient_utils
[email protected]aa5ced12016-03-29 09:41:1450import gerrit_util
[email protected]151ebcf2016-03-09 01:08:2551import git_cache
[email protected]9e849272014-04-04 00:31:5552import git_common
[email protected]09d7a6a2016-03-04 15:44:4853import git_footers
[email protected]336f9122014-09-04 02:16:5554import owners
[email protected]9e849272014-04-04 00:31:5555import owners_finder
[email protected]2a74d372011-03-29 19:05:5056import presubmit_support
[email protected]cab38e92011-04-09 00:30:5157import rietveld
[email protected]2a74d372011-03-29 19:05:5058import scm
[email protected]0633fb42013-08-16 20:06:1459import subcommand
[email protected]32f9f5e2011-09-14 13:41:4760import subprocess2
[email protected]2a74d372011-03-29 19:05:5061import watchlists
62
[email protected]0633fb42013-08-16 20:06:1463__version__ = '1.0'
[email protected]2a74d372011-03-29 19:05:5064
[email protected]eb5edbc2012-01-16 17:03:2865DEFAULT_SERVER = 'https://codereview.appspot.com'
[email protected]0ba7f962011-01-11 22:13:5866POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
[email protected]cc51cd02010-12-23 00:48:3967DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
[email protected]d6617f32013-11-19 00:34:5468GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
[email protected]c68112d2015-03-03 12:48:0669REFS_THAT_ALIAS_TO_OTHER_REFS = {
70 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
71 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
72}
[email protected]cc51cd02010-12-23 00:48:3973
[email protected]44202a22014-03-11 19:22:1874# Valid extensions for files we want to lint.
75DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
76DEFAULT_LINT_IGNORE_REGEX = r"$^"
77
[email protected]2e23ce32013-05-07 12:42:2878# Shortcut since it quickly becomes redundant.
79Fore = colorama.Fore
[email protected]90541732011-04-01 17:54:1880
[email protected]ddd59412011-11-30 14:20:3881# Initialized in main()
82settings = None
83
84
[email protected]cc51cd02010-12-23 00:48:3985def DieWithError(message):
[email protected]970c5222011-03-12 00:32:2486 print >> sys.stderr, message
[email protected]cc51cd02010-12-23 00:48:3987 sys.exit(1)
88
89
[email protected]8b0553c2014-02-11 00:33:3790def GetNoGitPagerEnv():
91 env = os.environ.copy()
92 # 'cat' is a magical git string that disables pagers on all platforms.
93 env['GIT_PAGER'] = 'cat'
94 return env
95
[email protected]566a02a2014-08-22 01:34:1396
[email protected]627d9002016-04-29 00:00:5297def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
[email protected]cc51cd02010-12-23 00:48:3998 try:
[email protected]627d9002016-04-29 00:00:5299 return subprocess2.check_output(args, shell=shell, **kwargs)
[email protected]78936cb2013-04-11 00:17:52100 except subprocess2.CalledProcessError as e:
101 logging.debug('Failed running %s', args)
[email protected]32f9f5e2011-09-14 13:41:47102 if not error_ok:
[email protected]cc51cd02010-12-23 00:48:39103 DieWithError(
[email protected]32f9f5e2011-09-14 13:41:47104 'Command "%s" failed.\n%s' % (
105 ' '.join(args), error_message or e.stdout or ''))
106 return e.stdout
[email protected]cc51cd02010-12-23 00:48:39107
108
109def RunGit(args, **kwargs):
[email protected]32f9f5e2011-09-14 13:41:47110 """Returns stdout."""
[email protected]82b91cd2013-07-09 06:33:41111 return RunCommand(['git'] + args, **kwargs)
[email protected]cc51cd02010-12-23 00:48:39112
113
[email protected]3b7e15c2014-01-21 17:44:47114def RunGitWithCode(args, suppress_stderr=False):
[email protected]32f9f5e2011-09-14 13:41:47115 """Returns return code and stdout."""
[email protected]9bb85e22012-06-13 20:28:23116 try:
[email protected]3b7e15c2014-01-21 17:44:47117 if suppress_stderr:
118 stderr = subprocess2.VOID
119 else:
120 stderr = sys.stderr
[email protected]82b91cd2013-07-09 06:33:41121 out, code = subprocess2.communicate(['git'] + args,
[email protected]8b0553c2014-02-11 00:33:37122 env=GetNoGitPagerEnv(),
[email protected]3b7e15c2014-01-21 17:44:47123 stdout=subprocess2.PIPE,
124 stderr=stderr)
[email protected]9bb85e22012-06-13 20:28:23125 return code, out[0]
126 except ValueError:
127 # When the subprocess fails, it returns None. That triggers a ValueError
128 # when trying to unpack the return value into (out, code).
129 return 1, ''
[email protected]cc51cd02010-12-23 00:48:39130
131
[email protected]27386dd2015-02-16 10:45:39132def RunGitSilent(args):
[email protected]cbd7dc32016-05-31 10:33:50133 """Returns stdout, suppresses stderr and ignores the return code."""
[email protected]27386dd2015-02-16 10:45:39134 return RunGitWithCode(args, suppress_stderr=True)[1]
135
136
[email protected]6a0b07c2013-07-10 01:29:19137def IsGitVersionAtLeast(min_version):
[email protected]cc56ee42013-07-10 22:16:29138 prefix = 'git version '
[email protected]6a0b07c2013-07-10 01:29:19139 version = RunGit(['--version']).strip()
[email protected]cc56ee42013-07-10 22:16:29140 return (version.startswith(prefix) and
141 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
[email protected]6a0b07c2013-07-10 01:29:19142
143
[email protected]8ba38ff2015-06-11 21:41:25144def BranchExists(branch):
145 """Return True if specified branch exists."""
146 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
147 suppress_stderr=True)
148 return not code
149
150
[email protected]90541732011-04-01 17:54:18151def ask_for_data(prompt):
152 try:
153 return raw_input(prompt)
154 except KeyboardInterrupt:
155 # Hide the exception.
156 sys.exit(1)
157
158
[email protected]79540052012-10-19 23:15:26159def git_set_branch_value(key, value):
[email protected]aa5ced12016-03-29 09:41:14160 branch = GetCurrentBranch()
[email protected]caa16552013-03-18 20:45:05161 if not branch:
162 return
163
164 cmd = ['config']
165 if isinstance(value, int):
166 cmd.append('--int')
167 git_key = 'branch.%s.%s' % (branch, key)
168 RunGit(cmd + [git_key, str(value)])
[email protected]79540052012-10-19 23:15:26169
170
171def git_get_branch_default(key, default):
[email protected]aa5ced12016-03-29 09:41:14172 branch = GetCurrentBranch()
[email protected]79540052012-10-19 23:15:26173 if branch:
174 git_key = 'branch.%s.%s' % (branch, key)
175 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
176 try:
177 return int(stdout.strip())
178 except ValueError:
179 pass
180 return default
181
182
[email protected]53937ba2012-10-02 18:20:43183def add_git_similarity(parser):
184 parser.add_option(
[email protected]79540052012-10-19 23:15:26185 '--similarity', metavar='SIM', type='int', action='store',
[email protected]53937ba2012-10-02 18:20:43186 help='Sets the percentage that a pair of files need to match in order to'
187 ' be considered copies (default 50)')
[email protected]79540052012-10-19 23:15:26188 parser.add_option(
189 '--find-copies', action='store_true',
190 help='Allows git to look for copies.')
191 parser.add_option(
192 '--no-find-copies', action='store_false', dest='find_copies',
193 help='Disallows git from looking for copies.')
[email protected]53937ba2012-10-02 18:20:43194
195 old_parser_args = parser.parse_args
196 def Parse(args):
197 options, args = old_parser_args(args)
198
[email protected]53937ba2012-10-02 18:20:43199 if options.similarity is None:
[email protected]79540052012-10-19 23:15:26200 options.similarity = git_get_branch_default('git-cl-similarity', 50)
[email protected]53937ba2012-10-02 18:20:43201 else:
[email protected]79540052012-10-19 23:15:26202 print('Note: Saving similarity of %d%% in git config.'
203 % options.similarity)
204 git_set_branch_value('git-cl-similarity', options.similarity)
[email protected]53937ba2012-10-02 18:20:43205
[email protected]79540052012-10-19 23:15:26206 options.similarity = max(0, min(options.similarity, 100))
207
208 if options.find_copies is None:
209 options.find_copies = bool(
210 git_get_branch_default('git-find-copies', True))
211 else:
212 git_set_branch_value('git-find-copies', int(options.find_copies))
[email protected]53937ba2012-10-02 18:20:43213
214 print('Using %d%% similarity for rename/copy detection. '
215 'Override with --similarity.' % options.similarity)
216
217 return options, args
218 parser.parse_args = Parse
219
220
[email protected]45453142015-09-15 08:45:22221def _get_properties_from_options(options):
222 properties = dict(x.split('=', 1) for x in options.properties)
223 for key, val in properties.iteritems():
224 try:
225 properties[key] = json.loads(val)
226 except ValueError:
227 pass # If a value couldn't be evaluated, treat it as a string.
228 return properties
229
230
[email protected]6ebaf782015-05-12 19:17:54231def _prefix_master(master):
232 """Convert user-specified master name to full master name.
233
234 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
235 name, while the developers always use shortened master name
236 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
237 function does the conversion for buildbucket migration.
238 """
239 prefix = 'master.'
240 if master.startswith(prefix):
241 return master
242 return '%s%s' % (prefix, master)
243
244
[email protected]b015fac2016-02-26 14:52:01245def _buildbucket_retry(operation_name, http, *args, **kwargs):
246 """Retries requests to buildbucket service and returns parsed json content."""
247 try_count = 0
248 while True:
249 response, content = http.request(*args, **kwargs)
250 try:
251 content_json = json.loads(content)
252 except ValueError:
253 content_json = None
254
255 # Buildbucket could return an error even if status==200.
256 if content_json and content_json.get('error'):
[email protected]baff4e12016-03-08 00:33:57257 error = content_json.get('error')
258 if error.get('code') == 403:
259 raise BuildbucketResponseException(
260 'Access denied: %s' % error.get('message', ''))
[email protected]b015fac2016-02-26 14:52:01261 msg = 'Error in response. Reason: %s. Message: %s.' % (
[email protected]baff4e12016-03-08 00:33:57262 error.get('reason', ''), error.get('message', ''))
[email protected]b015fac2016-02-26 14:52:01263 raise BuildbucketResponseException(msg)
264
265 if response.status == 200:
266 if not content_json:
267 raise BuildbucketResponseException(
268 'Buildbucket returns invalid json content: %s.\n'
269 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
270 content)
271 return content_json
272 if response.status < 500 or try_count >= 2:
273 raise httplib2.HttpLib2Error(content)
274
275 # status >= 500 means transient failures.
276 logging.debug('Transient errors when %s. Will retry.', operation_name)
277 time.sleep(0.5 + 1.5*try_count)
278 try_count += 1
279 assert False, 'unreachable'
280
281
[email protected]feb9e2a2015-09-25 19:11:09282def trigger_luci_job(changelist, masters, options):
283 """Send a job to run on LUCI."""
284 issue_props = changelist.GetIssueProperties()
285 issue = changelist.GetIssue()
286 patchset = changelist.GetMostRecentPatchset()
287 for builders_and_tests in sorted(masters.itervalues()):
[email protected]3764fa22015-10-21 16:40:40288 # TODO(hinoka et al): add support for other properties.
289 # Currently, this completely ignores testfilter and other properties.
290 for builder in sorted(builders_and_tests):
[email protected]feb9e2a2015-09-25 19:11:09291 luci_trigger.trigger(
292 builder, 'HEAD', issue, patchset, issue_props['project'])
293
294
[email protected]45453142015-09-15 08:45:22295def trigger_try_jobs(auth_config, changelist, options, masters, category):
[email protected]6ebaf782015-05-12 19:17:54296 rietveld_url = settings.GetDefaultServerUrl()
297 rietveld_host = urlparse.urlparse(rietveld_url).hostname
298 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
299 http = authenticator.authorize(httplib2.Http())
300 http.force_exception_to_status_code = True
301 issue_props = changelist.GetIssueProperties()
302 issue = changelist.GetIssue()
303 patchset = changelist.GetMostRecentPatchset()
[email protected]45453142015-09-15 08:45:22304 properties = _get_properties_from_options(options)
[email protected]6ebaf782015-05-12 19:17:54305
306 buildbucket_put_url = (
307 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
[email protected]db375572015-08-17 19:22:23308 hostname=options.buildbucket_host))
[email protected]6ebaf782015-05-12 19:17:54309 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
310 hostname=rietveld_host,
311 issue=issue,
312 patch=patchset)
313
314 batch_req_body = {'builds': []}
315 print_text = []
316 print_text.append('Tried jobs on:')
317 for master, builders_and_tests in sorted(masters.iteritems()):
318 print_text.append('Master: %s' % master)
319 bucket = _prefix_master(master)
320 for builder, tests in sorted(builders_and_tests.iteritems()):
321 print_text.append(' %s: %s' % (builder, tests))
322 parameters = {
323 'builder_name': builder,
[email protected]d2217312015-09-21 15:51:21324 'changes': [{
325 'author': {'email': issue_props['owner_email']},
326 'revision': options.revision,
327 }],
[email protected]6ebaf782015-05-12 19:17:54328 'properties': {
329 'category': category,
330 'issue': issue,
331 'master': master,
332 'patch_project': issue_props['project'],
333 'patch_storage': 'rietveld',
334 'patchset': patchset,
335 'reason': options.name,
[email protected]6ebaf782015-05-12 19:17:54336 'rietveld': rietveld_url,
[email protected]6ebaf782015-05-12 19:17:54337 },
338 }
[email protected]2403e802016-04-29 12:34:42339 if 'presubmit' in builder.lower():
340 parameters['properties']['dry_run'] = 'true'
[email protected]3764fa22015-10-21 16:40:40341 if tests:
342 parameters['properties']['testfilter'] = tests
[email protected]45453142015-09-15 08:45:22343 if properties:
344 parameters['properties'].update(properties)
[email protected]6ebaf782015-05-12 19:17:54345 if options.clobber:
346 parameters['properties']['clobber'] = True
347 batch_req_body['builds'].append(
348 {
349 'bucket': bucket,
350 'parameters_json': json.dumps(parameters),
[email protected]b015fac2016-02-26 14:52:01351 'client_operation_id': str(uuid.uuid4()),
[email protected]6ebaf782015-05-12 19:17:54352 'tags': ['builder:%s' % builder,
353 'buildset:%s' % buildset,
354 'master:%s' % master,
355 'user_agent:git_cl_try']
356 }
357 )
358
[email protected]b015fac2016-02-26 14:52:01359 _buildbucket_retry(
360 'triggering tryjobs',
361 http,
362 buildbucket_put_url,
363 'PUT',
364 body=json.dumps(batch_req_body),
365 headers={'Content-Type': 'application/json'}
366 )
[email protected]35c61452016-02-26 15:24:57367 print_text.append('To see results here, run: git cl try-results')
368 print_text.append('To see results in browser, run: git cl web')
[email protected]6ebaf782015-05-12 19:17:54369 print '\n'.join(print_text)
[email protected]44424542015-06-02 18:35:29370
[email protected]6ebaf782015-05-12 19:17:54371
[email protected]b015fac2016-02-26 14:52:01372def fetch_try_jobs(auth_config, changelist, options):
373 """Fetches tryjobs from buildbucket.
374
375 Returns a map from build id to build info as json dictionary.
376 """
377 rietveld_url = settings.GetDefaultServerUrl()
378 rietveld_host = urlparse.urlparse(rietveld_url).hostname
379 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
380 if authenticator.has_cached_credentials():
381 http = authenticator.authorize(httplib2.Http())
382 else:
383 print ('Warning: Some results might be missing because %s' %
384 # Get the message on how to login.
385 auth.LoginRequiredError(rietveld_host).message)
386 http = httplib2.Http()
387
388 http.force_exception_to_status_code = True
389
390 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
391 hostname=rietveld_host,
392 issue=changelist.GetIssue(),
393 patch=options.patchset)
394 params = {'tag': 'buildset:%s' % buildset}
395
396 builds = {}
397 while True:
398 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
399 hostname=options.buildbucket_host,
400 params=urllib.urlencode(params))
401 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
402 for build in content.get('builds', []):
403 builds[build['id']] = build
404 if 'next_cursor' in content:
405 params['start_cursor'] = content['next_cursor']
406 else:
407 break
408 return builds
409
410
411def print_tryjobs(options, builds):
412 """Prints nicely result of fetch_try_jobs."""
413 if not builds:
414 print 'No tryjobs scheduled'
415 return
416
417 # Make a copy, because we'll be modifying builds dictionary.
418 builds = builds.copy()
419 builder_names_cache = {}
420
421 def get_builder(b):
422 try:
423 return builder_names_cache[b['id']]
424 except KeyError:
425 try:
426 parameters = json.loads(b['parameters_json'])
427 name = parameters['builder_name']
428 except (ValueError, KeyError) as error:
429 print 'WARNING: failed to get builder name for build %s: %s' % (
430 b['id'], error)
431 name = None
432 builder_names_cache[b['id']] = name
433 return name
434
435 def get_bucket(b):
436 bucket = b['bucket']
437 if bucket.startswith('master.'):
438 return bucket[len('master.'):]
439 return bucket
440
441 if options.print_master:
442 name_fmt = '%%-%ds %%-%ds' % (
443 max(len(str(get_bucket(b))) for b in builds.itervalues()),
444 max(len(str(get_builder(b))) for b in builds.itervalues()))
445 def get_name(b):
446 return name_fmt % (get_bucket(b), get_builder(b))
447 else:
448 name_fmt = '%%-%ds' % (
449 max(len(str(get_builder(b))) for b in builds.itervalues()))
450 def get_name(b):
451 return name_fmt % get_builder(b)
452
453 def sort_key(b):
454 return b['status'], b.get('result'), get_name(b), b.get('url')
455
456 def pop(title, f, color=None, **kwargs):
457 """Pop matching builds from `builds` dict and print them."""
458
[email protected]6cf98c82016-03-15 11:56:00459 if not options.color or color is None:
[email protected]b015fac2016-02-26 14:52:01460 colorize = str
461 else:
462 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
463
464 result = []
465 for b in builds.values():
466 if all(b.get(k) == v for k, v in kwargs.iteritems()):
467 builds.pop(b['id'])
468 result.append(b)
469 if result:
470 print colorize(title)
471 for b in sorted(result, key=sort_key):
472 print ' ', colorize('\t'.join(map(str, f(b))))
473
474 total = len(builds)
475 pop(status='COMPLETED', result='SUCCESS',
476 title='Successes:', color=Fore.GREEN,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
479 title='Infra Failures:', color=Fore.MAGENTA,
480 f=lambda b: (get_name(b), b.get('url')))
481 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
482 title='Failures:', color=Fore.RED,
483 f=lambda b: (get_name(b), b.get('url')))
484 pop(status='COMPLETED', result='CANCELED',
485 title='Canceled:', color=Fore.MAGENTA,
486 f=lambda b: (get_name(b),))
487 pop(status='COMPLETED', result='FAILURE',
488 failure_reason='INVALID_BUILD_DEFINITION',
489 title='Wrong master/builder name:', color=Fore.MAGENTA,
490 f=lambda b: (get_name(b),))
491 pop(status='COMPLETED', result='FAILURE',
492 title='Other failures:',
493 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
494 pop(status='COMPLETED',
495 title='Other finished:',
496 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
497 pop(status='STARTED',
498 title='Started:', color=Fore.YELLOW,
499 f=lambda b: (get_name(b), b.get('url')))
500 pop(status='SCHEDULED',
501 title='Scheduled:',
502 f=lambda b: (get_name(b), 'id=%s' % b['id']))
503 # The last section is just in case buildbucket API changes OR there is a bug.
504 pop(title='Other:',
505 f=lambda b: (get_name(b), 'id=%s' % b['id']))
506 assert len(builds) == 0
507 print 'Total: %d tryjobs' % total
508
509
[email protected]866276c2011-03-18 20:09:31510def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
511 """Return the corresponding git ref if |base_url| together with |glob_spec|
512 matches the full |url|.
513
514 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
515 """
516 fetch_suburl, as_ref = glob_spec.split(':')
517 if allow_wildcards:
518 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
519 if glob_match:
520 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
521 # "branches/{472,597,648}/src:refs/remotes/svn/*".
522 branch_re = re.escape(base_url)
523 if glob_match.group(1):
524 branch_re += '/' + re.escape(glob_match.group(1))
525 wildcard = glob_match.group(2)
526 if wildcard == '*':
527 branch_re += '([^/]*)'
528 else:
529 # Escape and replace surrounding braces with parentheses and commas
530 # with pipe symbols.
531 wildcard = re.escape(wildcard)
532 wildcard = re.sub('^\\\\{', '(', wildcard)
533 wildcard = re.sub('\\\\,', '|', wildcard)
534 wildcard = re.sub('\\\\}$', ')', wildcard)
535 branch_re += wildcard
536 if glob_match.group(3):
537 branch_re += re.escape(glob_match.group(3))
538 match = re.match(branch_re, url)
539 if match:
540 return re.sub('\*$', match.group(1), as_ref)
541
542 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
543 if fetch_suburl:
544 full_url = base_url + '/' + fetch_suburl
545 else:
546 full_url = base_url
547 if full_url == url:
548 return as_ref
549 return None
550
[email protected]32f9f5e2011-09-14 13:41:47551
[email protected]79540052012-10-19 23:15:26552def print_stats(similarity, find_copies, args):
[email protected]49e3d802012-07-18 23:54:45553 """Prints statistics about the change to the user."""
554 # --no-ext-diff is broken in some versions of Git, so try to work around
555 # this by overriding the environment (but there is still a problem if the
556 # git config key "diff.external" is used).
[email protected]8b0553c2014-02-11 00:33:37557 env = GetNoGitPagerEnv()
[email protected]49e3d802012-07-18 23:54:45558 if 'GIT_EXTERNAL_DIFF' in env:
559 del env['GIT_EXTERNAL_DIFF']
[email protected]79540052012-10-19 23:15:26560
561 if find_copies:
562 similarity_options = ['--find-copies-harder', '-l100000',
563 '-C%s' % similarity]
564 else:
565 similarity_options = ['-M%s' % similarity]
566
[email protected]d057f9a2014-05-29 21:09:36567 try:
568 stdout = sys.stdout.fileno()
569 except AttributeError:
570 stdout = None
[email protected]49e3d802012-07-18 23:54:45571 return subprocess2.call(
[email protected]82b91cd2013-07-09 06:33:41572 ['git',
[email protected]f267b0e2013-05-02 09:11:43573 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
[email protected]d057f9a2014-05-29 21:09:36574 stdout=stdout, env=env)
[email protected]49e3d802012-07-18 23:54:45575
576
[email protected]6ebaf782015-05-12 19:17:54577class BuildbucketResponseException(Exception):
578 pass
579
580
[email protected]cc51cd02010-12-23 00:48:39581class Settings(object):
582 def __init__(self):
583 self.default_server = None
584 self.cc = None
[email protected]7a54e812014-02-11 19:57:22585 self.root = None
[email protected]cc51cd02010-12-23 00:48:39586 self.is_git_svn = None
587 self.svn_branch = None
588 self.tree_status_url = None
589 self.viewvc_url = None
590 self.updated = False
[email protected]e8077812012-02-03 03:41:46591 self.is_gerrit = None
[email protected]54b400c2016-01-14 10:08:25592 self.squash_gerrit_uploads = None
[email protected]28253532016-04-14 13:46:56593 self.gerrit_skip_ensure_authenticated = None
[email protected]615a2622013-05-03 13:20:14594 self.git_editor = None
[email protected]152cf832014-06-11 21:37:49595 self.project = None
[email protected]6abc6522014-12-02 07:34:49596 self.force_https_commit_url = None
[email protected]566a02a2014-08-22 01:34:13597 self.pending_ref_prefix = None
[email protected]cc51cd02010-12-23 00:48:39598
599 def LazyUpdateIfNeeded(self):
600 """Updates the settings from a codereview.settings file, if available."""
601 if not self.updated:
[email protected]87884cc2014-01-03 22:23:41602 # The only value that actually changes the behavior is
603 # autoupdate = "false". Everything else means "true".
[email protected]3ac1c4e2014-01-16 02:44:42604 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
[email protected]87884cc2014-01-03 22:23:41605 error_ok=True
606 ).strip().lower()
607
[email protected]cc51cd02010-12-23 00:48:39608 cr_settings_file = FindCodereviewSettingsFile()
[email protected]87884cc2014-01-03 22:23:41609 if autoupdate != 'false' and cr_settings_file:
[email protected]cc51cd02010-12-23 00:48:39610 LoadCodereviewSettingsFromFile(cr_settings_file)
[email protected]cc51cd02010-12-23 00:48:39611 self.updated = True
612
613 def GetDefaultServerUrl(self, error_ok=False):
614 if not self.default_server:
615 self.LazyUpdateIfNeeded()
[email protected]eb5edbc2012-01-16 17:03:28616 self.default_server = gclient_utils.UpgradeToHttps(
[email protected]8b0553c2014-02-11 00:33:37617 self._GetRietveldConfig('server', error_ok=True))
[email protected]cc51cd02010-12-23 00:48:39618 if error_ok:
619 return self.default_server
620 if not self.default_server:
621 error_message = ('Could not find settings file. You must configure '
622 'your review setup by running "git cl config".')
[email protected]eb5edbc2012-01-16 17:03:28623 self.default_server = gclient_utils.UpgradeToHttps(
[email protected]8b0553c2014-02-11 00:33:37624 self._GetRietveldConfig('server', error_message=error_message))
[email protected]cc51cd02010-12-23 00:48:39625 return self.default_server
626
[email protected]7a54e812014-02-11 19:57:22627 @staticmethod
628 def GetRelativeRoot():
629 return RunGit(['rev-parse', '--show-cdup']).strip()
[email protected]8b0553c2014-02-11 00:33:37630
[email protected]cc51cd02010-12-23 00:48:39631 def GetRoot(self):
[email protected]7a54e812014-02-11 19:57:22632 if self.root is None:
633 self.root = os.path.abspath(self.GetRelativeRoot())
634 return self.root
[email protected]cc51cd02010-12-23 00:48:39635
[email protected]151ebcf2016-03-09 01:08:25636 def GetGitMirror(self, remote='origin'):
637 """If this checkout is from a local git mirror, return a Mirror object."""
[email protected]81593742016-03-09 20:27:58638 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
[email protected]151ebcf2016-03-09 01:08:25639 if not os.path.isdir(local_url):
640 return None
641 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
642 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
643 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
644 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
645 if mirror.exists():
646 return mirror
647 return None
648
[email protected]cc51cd02010-12-23 00:48:39649 def GetIsGitSvn(self):
650 """Return true if this repo looks like it's using git-svn."""
651 if self.is_git_svn is None:
[email protected]566a02a2014-08-22 01:34:13652 if self.GetPendingRefPrefix():
653 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
654 self.is_git_svn = False
655 else:
656 # If you have any "svn-remote.*" config keys, we think you're using svn.
657 self.is_git_svn = RunGitWithCode(
658 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
[email protected]cc51cd02010-12-23 00:48:39659 return self.is_git_svn
660
661 def GetSVNBranch(self):
662 if self.svn_branch is None:
663 if not self.GetIsGitSvn():
664 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
665
666 # Try to figure out which remote branch we're based on.
667 # Strategy:
[email protected]ade368c2011-03-01 08:57:50668 # 1) iterate through our branch history and find the svn URL.
669 # 2) find the svn-remote that fetches from the URL.
[email protected]cc51cd02010-12-23 00:48:39670
671 # regexp matching the git-svn line that contains the URL.
672 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
673
[email protected]ade368c2011-03-01 08:57:50674 # We don't want to go through all of history, so read a line from the
675 # pipe at a time.
676 # The -100 is an arbitrary limit so we don't search forever.
[email protected]82b91cd2013-07-09 06:33:41677 cmd = ['git', 'log', '-100', '--pretty=medium']
[email protected]8b0553c2014-02-11 00:33:37678 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
679 env=GetNoGitPagerEnv())
[email protected]740f9d72011-06-10 18:33:10680 url = None
[email protected]ade368c2011-03-01 08:57:50681 for line in proc.stdout:
682 match = git_svn_re.match(line)
683 if match:
684 url = match.group(1)
685 proc.stdout.close() # Cut pipe.
686 break
[email protected]cc51cd02010-12-23 00:48:39687
[email protected]ade368c2011-03-01 08:57:50688 if url:
689 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
690 remotes = RunGit(['config', '--get-regexp',
691 r'^svn-remote\..*\.url']).splitlines()
692 for remote in remotes:
693 match = svn_remote_re.match(remote)
[email protected]cc51cd02010-12-23 00:48:39694 if match:
[email protected]ade368c2011-03-01 08:57:50695 remote = match.group(1)
696 base_url = match.group(2)
[email protected]4ac25532013-12-16 22:07:02697 rewrite_root = RunGit(
698 ['config', 'svn-remote.%s.rewriteRoot' % remote],
699 error_ok=True).strip()
700 if rewrite_root:
701 base_url = rewrite_root
[email protected]ade368c2011-03-01 08:57:50702 fetch_spec = RunGit(
[email protected]866276c2011-03-18 20:09:31703 ['config', 'svn-remote.%s.fetch' % remote],
704 error_ok=True).strip()
705 if fetch_spec:
706 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
707 if self.svn_branch:
708 break
709 branch_spec = RunGit(
710 ['config', 'svn-remote.%s.branches' % remote],
711 error_ok=True).strip()
712 if branch_spec:
713 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
714 if self.svn_branch:
715 break
716 tag_spec = RunGit(
717 ['config', 'svn-remote.%s.tags' % remote],
718 error_ok=True).strip()
719 if tag_spec:
720 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
721 if self.svn_branch:
722 break
[email protected]cc51cd02010-12-23 00:48:39723
724 if not self.svn_branch:
725 DieWithError('Can\'t guess svn branch -- try specifying it on the '
726 'command line')
727
728 return self.svn_branch
729
730 def GetTreeStatusUrl(self, error_ok=False):
731 if not self.tree_status_url:
732 error_message = ('You must configure your tree status URL by running '
733 '"git cl config".')
[email protected]8b0553c2014-02-11 00:33:37734 self.tree_status_url = self._GetRietveldConfig(
735 'tree-status-url', error_ok=error_ok, error_message=error_message)
[email protected]cc51cd02010-12-23 00:48:39736 return self.tree_status_url
737
738 def GetViewVCUrl(self):
739 if not self.viewvc_url:
[email protected]8b0553c2014-02-11 00:33:37740 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
[email protected]cc51cd02010-12-23 00:48:39741 return self.viewvc_url
742
[email protected]90752582014-01-14 21:04:50743 def GetBugPrefix(self):
[email protected]8b0553c2014-02-11 00:33:37744 return self._GetRietveldConfig('bug-prefix', error_ok=True)
[email protected]90752582014-01-14 21:04:50745
[email protected]78948ed2015-07-08 23:09:57746 def GetIsSkipDependencyUpload(self, branch_name):
747 """Returns true if specified branch should skip dep uploads."""
748 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
749 error_ok=True)
750
[email protected]5626a922015-02-26 14:03:30751 def GetRunPostUploadHook(self):
752 run_post_upload_hook = self._GetRietveldConfig(
753 'run-post-upload-hook', error_ok=True)
754 return run_post_upload_hook == "True"
755
[email protected]ae6df352011-04-06 17:40:39756 def GetDefaultCCList(self):
[email protected]8b0553c2014-02-11 00:33:37757 return self._GetRietveldConfig('cc', error_ok=True)
[email protected]ae6df352011-04-06 17:40:39758
[email protected]c1737d02013-05-29 14:17:28759 def GetDefaultPrivateFlag(self):
[email protected]8b0553c2014-02-11 00:33:37760 return self._GetRietveldConfig('private', error_ok=True)
[email protected]c1737d02013-05-29 14:17:28761
[email protected]e8077812012-02-03 03:41:46762 def GetIsGerrit(self):
763 """Return true if this repo is assosiated with gerrit code review system."""
764 if self.is_gerrit is None:
765 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
766 return self.is_gerrit
767
[email protected]54b400c2016-01-14 10:08:25768 def GetSquashGerritUploads(self):
769 """Return true if uploads to Gerrit should be squashed by default."""
770 if self.squash_gerrit_uploads is None:
771 self.squash_gerrit_uploads = (
772 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
773 error_ok=True).strip() == 'true')
774 return self.squash_gerrit_uploads
775
[email protected]28253532016-04-14 13:46:56776 def GetGerritSkipEnsureAuthenticated(self):
777 """Return True if EnsureAuthenticated should not be done for Gerrit
778 uploads."""
779 if self.gerrit_skip_ensure_authenticated is None:
780 self.gerrit_skip_ensure_authenticated = (
[email protected]00dbccd2016-04-15 07:24:43781 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
[email protected]28253532016-04-14 13:46:56782 error_ok=True).strip() == 'true')
783 return self.gerrit_skip_ensure_authenticated
784
[email protected]615a2622013-05-03 13:20:14785 def GetGitEditor(self):
786 """Return the editor specified in the git config, or None if none is."""
787 if self.git_editor is None:
788 self.git_editor = self._GetConfig('core.editor', error_ok=True)
789 return self.git_editor or None
790
[email protected]44202a22014-03-11 19:22:18791 def GetLintRegex(self):
792 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
793 DEFAULT_LINT_REGEX)
794
795 def GetLintIgnoreRegex(self):
796 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
797 DEFAULT_LINT_IGNORE_REGEX)
798
[email protected]152cf832014-06-11 21:37:49799 def GetProject(self):
800 if not self.project:
801 self.project = self._GetRietveldConfig('project', error_ok=True)
802 return self.project
803
[email protected]6abc6522014-12-02 07:34:49804 def GetForceHttpsCommitUrl(self):
805 if not self.force_https_commit_url:
806 self.force_https_commit_url = self._GetRietveldConfig(
807 'force-https-commit-url', error_ok=True)
808 return self.force_https_commit_url
809
[email protected]566a02a2014-08-22 01:34:13810 def GetPendingRefPrefix(self):
811 if not self.pending_ref_prefix:
812 self.pending_ref_prefix = self._GetRietveldConfig(
813 'pending-ref-prefix', error_ok=True)
814 return self.pending_ref_prefix
815
[email protected]8b0553c2014-02-11 00:33:37816 def _GetRietveldConfig(self, param, **kwargs):
817 return self._GetConfig('rietveld.' + param, **kwargs)
818
[email protected]78948ed2015-07-08 23:09:57819 def _GetBranchConfig(self, branch_name, param, **kwargs):
820 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
821
[email protected]cc51cd02010-12-23 00:48:39822 def _GetConfig(self, param, **kwargs):
823 self.LazyUpdateIfNeeded()
824 return RunGit(['config', param], **kwargs).strip()
825
826
[email protected]cc51cd02010-12-23 00:48:39827def ShortBranchName(branch):
828 """Convert a name like 'refs/heads/foo' to just 'foo'."""
[email protected]aa5ced12016-03-29 09:41:14829 return branch.replace('refs/heads/', '', 1)
830
831
832def GetCurrentBranchRef():
833 """Returns branch ref (e.g., refs/heads/master) or None."""
834 return RunGit(['symbolic-ref', 'HEAD'],
835 stderr=subprocess2.VOID, error_ok=True).strip() or None
836
837
838def GetCurrentBranch():
839 """Returns current branch or None.
840
841 For refs/heads/* branches, returns just last part. For others, full ref.
842 """
843 branchref = GetCurrentBranchRef()
844 if branchref:
845 return ShortBranchName(branchref)
846 return None
[email protected]cc51cd02010-12-23 00:48:39847
848
[email protected]fa330e82016-04-13 17:09:52849class _CQState(object):
850 """Enum for states of CL with respect to Commit Queue."""
851 NONE = 'none'
852 DRY_RUN = 'dry_run'
853 COMMIT = 'commit'
854
855 ALL_STATES = [NONE, DRY_RUN, COMMIT]
856
857
[email protected]f86c7d32016-04-01 19:27:30858class _ParsedIssueNumberArgument(object):
859 def __init__(self, issue=None, patchset=None, hostname=None):
860 self.issue = issue
861 self.patchset = patchset
862 self.hostname = hostname
863
864 @property
865 def valid(self):
866 return self.issue is not None
867
868
869class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
870 def __init__(self, *args, **kwargs):
871 self.patch_url = kwargs.pop('patch_url', None)
872 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
873
874
875def ParseIssueNumberArgument(arg):
876 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
877 fail_result = _ParsedIssueNumberArgument()
878
879 if arg.isdigit():
880 return _ParsedIssueNumberArgument(issue=int(arg))
881 if not arg.startswith('http'):
882 return fail_result
883 url = gclient_utils.UpgradeToHttps(arg)
884 try:
885 parsed_url = urlparse.urlparse(url)
886 except ValueError:
887 return fail_result
888 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
889 tmp = cls.ParseIssueURL(parsed_url)
890 if tmp is not None:
891 return tmp
892 return fail_result
893
894
[email protected]cc51cd02010-12-23 00:48:39895class Changelist(object):
[email protected]aa5ced12016-03-29 09:41:14896 """Changelist works with one changelist in local branch.
897
898 Supports two codereview backends: Rietveld or Gerrit, selected at object
899 creation.
900
[email protected]8930b3d2016-04-13 14:47:02901 Notes:
902 * Not safe for concurrent multi-{thread,process} use.
903 * Caches values from current branch. Therefore, re-use after branch change
904 with care.
[email protected]aa5ced12016-03-29 09:41:14905 """
906
907 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
908 """Create a new ChangeList instance.
909
910 If issue is given, the codereview must be given too.
911
912 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
913 Otherwise, it's decided based on current configuration of the local branch,
914 with default being 'rietveld' for backwards compatibility.
915 See _load_codereview_impl for more details.
916
917 **kwargs will be passed directly to codereview implementation.
918 """
[email protected]cc51cd02010-12-23 00:48:39919 # Poke settings so we get the "configure your server" message if necessary.
[email protected]379d07a2011-11-30 14:58:10920 global settings
921 if not settings:
922 # Happens when git_cl.py is used as a utility library.
923 settings = Settings()
[email protected]aa5ced12016-03-29 09:41:14924
925 if issue:
926 assert codereview, 'codereview must be known, if issue is known'
927
[email protected]cc51cd02010-12-23 00:48:39928 self.branchref = branchref
929 if self.branchref:
[email protected]cbd7dc32016-05-31 10:33:50930 assert branchref.startswith('refs/heads/')
[email protected]cc51cd02010-12-23 00:48:39931 self.branch = ShortBranchName(self.branchref)
932 else:
933 self.branch = None
[email protected]cc51cd02010-12-23 00:48:39934 self.upstream_branch = None
[email protected]1033efd2013-07-23 23:25:09935 self.lookedup_issue = False
936 self.issue = issue or None
[email protected]cc51cd02010-12-23 00:48:39937 self.has_description = False
938 self.description = None
[email protected]1033efd2013-07-23 23:25:09939 self.lookedup_patchset = False
[email protected]cc51cd02010-12-23 00:48:39940 self.patchset = None
[email protected]ae6df352011-04-06 17:40:39941 self.cc = None
942 self.watchers = ()
[email protected]cf6a5d22015-04-09 22:02:00943 self._remote = None
[email protected]cf6a5d22015-04-09 22:02:00944
[email protected]aa5ced12016-03-29 09:41:14945 self._codereview_impl = None
[email protected]d68b62b2016-03-31 16:09:29946 self._codereview = None
[email protected]aa5ced12016-03-29 09:41:14947 self._load_codereview_impl(codereview, **kwargs)
[email protected]d68b62b2016-03-31 16:09:29948 assert self._codereview_impl
949 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
[email protected]aa5ced12016-03-29 09:41:14950
951 def _load_codereview_impl(self, codereview=None, **kwargs):
952 if codereview:
[email protected]d68b62b2016-03-31 16:09:29953 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
954 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
955 self._codereview = codereview
956 self._codereview_impl = cls(self, **kwargs)
[email protected]aa5ced12016-03-29 09:41:14957 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
[email protected]d68b62b2016-03-31 16:09:29964 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
[email protected]aa5ced12016-03-29 09:41:14965 setting = cls.IssueSetting(self.GetBranch())
966 issue = RunGit(['config', setting], error_ok=True).strip()
967 if issue:
[email protected]d68b62b2016-03-31 16:09:29968 self._codereview = codereview
[email protected]aa5ced12016-03-29 09:41:14969 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
[email protected]d68b62b2016-03-31 16:09:29978 def IsGerrit(self):
979 return self._codereview == 'gerrit'
[email protected]ae6df352011-04-06 17:40:39980
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:
[email protected]99918ab2013-09-30 06:17:28987 base_cc = settings.GetDefaultCCList()
[email protected]ae6df352011-04-06 17:40:39988 more_cc = ','.join(self.watchers)
989 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
990 return self.cc
991
[email protected]99918ab2013-09-30 06:17:28992 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
[email protected]ae6df352011-04-06 17:40:39998 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
[email protected]cc51cd02010-12-23 00:48:391003
1004 def GetBranch(self):
1005 """Returns the short branch name, e.g. 'master'."""
1006 if not self.branch:
[email protected]aa5ced12016-03-29 09:41:141007 branchref = GetCurrentBranchRef()
[email protected]d62c61f2014-10-20 22:33:211008 if not branchref:
1009 return None
1010 self.branchref = branchref
[email protected]cc51cd02010-12-23 00:48:391011 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
[email protected]534f67a2016-04-07 18:47:051019 def ClearBranch(self):
1020 """Clears cached branch data of this object."""
1021 self.branch = self.branchref = None
1022
[email protected]0f58fa82012-11-05 01:45:201023 @staticmethod
1024 def FetchUpstreamTuple(branch):
[email protected]d6617f32013-11-19 00:34:541025 """Returns a tuple containing remote and remote ref,
[email protected]cc51cd02010-12-23 00:48:391026 e.g. 'origin', 'refs/heads/master'
1027 """
1028 remote = '.'
[email protected]cc51cd02010-12-23 00:48:391029 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:
[email protected]ade368c2011-03-01 08:57:501034 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1035 error_ok=True).strip()
1036 if upstream_branch:
1037 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
[email protected]cc51cd02010-12-23 00:48:391038 else:
[email protected]ade368c2011-03-01 08:57:501039 # Fall back on trying a git-svn upstream branch.
1040 if settings.GetIsGitSvn():
1041 upstream_branch = settings.GetSVNBranch()
[email protected]cc51cd02010-12-23 00:48:391042 else:
[email protected]ade368c2011-03-01 08:57:501043 # 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:
[email protected]aa5ced12016-03-29 09:41:141055 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 ...").')
[email protected]cc51cd02010-12-23 00:48:391061
1062 return remote, upstream_branch
1063
[email protected]8b0553c2014-02-11 00:33:371064 def GetCommonAncestorWithUpstream(self):
[email protected]8ba38ff2015-06-11 21:41:251065 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())
[email protected]9e849272014-04-04 00:31:551069 return git_common.get_or_create_merge_base(self.GetBranch(),
[email protected]8ba38ff2015-06-11 21:41:251070 upstream_branch)
[email protected]8b0553c2014-02-11 00:33:371071
[email protected]cc51cd02010-12-23 00:48:391072 def GetUpstreamBranch(self):
1073 if self.upstream_branch is None:
[email protected]0f58fa82012-11-05 01:45:201074 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
[email protected]cc51cd02010-12-23 00:48:391075 if remote is not '.':
[email protected]e7585452014-08-24 01:41:111076 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/')
[email protected]cc51cd02010-12-23 00:48:391080 self.upstream_branch = upstream_branch
1081 return self.upstream_branch
1082
[email protected]0f58fa82012-11-05 01:45:201083 def GetRemoteBranch(self):
[email protected]a2cbbbb2012-03-22 20:40:401084 if not self._remote:
[email protected]0f58fa82012-11-05 01:45:201085 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:
[email protected]a2cbbbb2012-03-22 20:40:401094 remotes = RunGit(['remote'], error_ok=True).split()
1095 if len(remotes) == 1:
[email protected]0f58fa82012-11-05 01:45:201096 remote, = remotes
[email protected]a2cbbbb2012-03-22 20:40:401097 elif 'origin' in remotes:
[email protected]0f58fa82012-11-05 01:45:201098 remote = 'origin'
[email protected]a2cbbbb2012-03-22 20:40:401099 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)
[email protected]0f58fa82012-11-05 01:45:201110 branch = 'HEAD'
1111 if branch.startswith('refs/remotes'):
1112 self._remote = (remote, branch)
[email protected]e7585452014-08-24 01:41:111113 elif branch.startswith('refs/branch-heads/'):
1114 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
[email protected]0f58fa82012-11-05 01:45:201115 else:
1116 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
[email protected]a2cbbbb2012-03-22 20:40:401117 return self._remote
1118
[email protected]0f58fa82012-11-05 01:45:201119 def GitSanityChecks(self, upstream_git_obj):
1120 """Checks git repo status and ensures diff is from local commits."""
1121
[email protected]79706062015-01-14 21:18:121122 if upstream_git_obj is None:
1123 if self.GetBranch() is None:
1124 print >> sys.stderr, (
[email protected]ee87f582015-07-31 18:46:251125 'ERROR: unable to determine current branch (detached HEAD?)')
[email protected]79706062015-01-14 21:18:121126 else:
1127 print >> sys.stderr, (
1128 'ERROR: no upstream branch')
1129 return False
1130
[email protected]0f58fa82012-11-05 01:45:201131 # 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
[email protected]6b0051e2012-04-03 15:45:081163 def GetGitBaseUrlFromConfig(self):
[email protected]a656e702014-05-15 20:43:051164 """Return the configured base URL from branch.<branchname>.baseurl.
[email protected]6b0051e2012-04-03 15:45:081165
1166 Returns None if it is not set.
1167 """
[email protected]a656e702014-05-15 20:43:051168 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1169 error_ok=True).strip()
[email protected]a2cbbbb2012-03-22 20:40:401170
[email protected]6abc6522014-12-02 07:34:491171 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
[email protected]cc51cd02010-12-23 00:48:391184 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 """
[email protected]0f58fa82012-11-05 01:45:201189 remote, _ = self.GetRemoteBranch()
[email protected]2a13d4f2014-06-13 00:06:371190 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
[email protected]cc51cd02010-12-23 00:48:391198
[email protected]87985d22016-03-24 17:33:331199 def GetIssue(self):
[email protected]52424302012-08-29 15:14:301200 """Returns the issue number as a int or None if not set."""
[email protected]87985d22016-03-24 17:33:331201 if self.issue is None and not self.lookedup_issue:
[email protected]aa5ced12016-03-29 09:41:141202 issue = RunGit(['config',
1203 self._codereview_impl.IssueSetting(self.GetBranch())],
1204 error_ok=True).strip()
[email protected]1033efd2013-07-23 23:25:091205 self.issue = int(issue) or None if issue else None
1206 self.lookedup_issue = True
[email protected]cc51cd02010-12-23 00:48:391207 return self.issue
1208
[email protected]cc51cd02010-12-23 00:48:391209 def GetIssueURL(self):
1210 """Get the URL for a particular issue."""
[email protected]aa5ced12016-03-29 09:41:141211 issue = self.GetIssue()
1212 if not issue:
[email protected]015fd3d2013-06-18 19:02:501213 return None
[email protected]aa5ced12016-03-29 09:41:141214 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
[email protected]cc51cd02010-12-23 00:48:391215
1216 def GetDescription(self, pretty=False):
1217 if not self.has_description:
1218 if self.GetIssue():
[email protected]aa5ced12016-03-29 09:41:141219 self.description = self._codereview_impl.FetchDescription()
[email protected]cc51cd02010-12-23 00:48:391220 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):
[email protected]52424302012-08-29 15:14:301228 """Returns the patchset number as a int or None if not set."""
[email protected]1033efd2013-07-23 23:25:091229 if self.patchset is None and not self.lookedup_patchset:
[email protected]aa5ced12016-03-29 09:41:141230 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
[email protected]cc51cd02010-12-23 00:48:391231 error_ok=True).strip()
[email protected]1033efd2013-07-23 23:25:091232 self.patchset = int(patchset) or None if patchset else None
1233 self.lookedup_patchset = True
[email protected]cc51cd02010-12-23 00:48:391234 return self.patchset
1235
1236 def SetPatchset(self, patchset):
1237 """Set this branch's patchset. If patchset=0, clears the patchset."""
[email protected]aa5ced12016-03-29 09:41:141238 patchset_setting = self._codereview_impl.PatchsetSetting()
[email protected]cc51cd02010-12-23 00:48:391239 if patchset:
[email protected]aa5ced12016-03-29 09:41:141240 RunGit(['config', patchset_setting, str(patchset)])
[email protected]1033efd2013-07-23 23:25:091241 self.patchset = patchset
[email protected]cc51cd02010-12-23 00:48:391242 else:
[email protected]aa5ced12016-03-29 09:41:141243 RunGit(['config', '--unset', patchset_setting],
[email protected]32f9f5e2011-09-14 13:41:471244 stderr=subprocess2.PIPE, error_ok=True)
[email protected]1033efd2013-07-23 23:25:091245 self.patchset = None
[email protected]cc51cd02010-12-23 00:48:391246
[email protected]a342c922016-03-16 07:08:251247 def SetIssue(self, issue=None):
1248 """Set this branch's issue. If issue isn't given, clears the issue."""
[email protected]aa5ced12016-03-29 09:41:141249 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1250 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
[email protected]cc51cd02010-12-23 00:48:391251 if issue:
[email protected]1033efd2013-07-23 23:25:091252 self.issue = issue
[email protected]aa5ced12016-03-29 09:41:141253 RunGit(['config', issue_setting, str(issue)])
1254 codereview_server = self._codereview_impl.GetCodereviewServer()
1255 if codereview_server:
1256 RunGit(['config', codereview_setting, codereview_server])
[email protected]cc51cd02010-12-23 00:48:391257 else:
[email protected]9b7fd712016-06-01 13:45:201258 # Reset it regardless. It doesn't hurt.
1259 config_settings = [issue_setting, self._codereview_impl.PatchsetSetting()]
1260 for prop in (['last-upload-hash'] +
1261 self._codereview_impl._PostUnsetIssueProperties()):
1262 config_settings.append('branch.%s.%s' % (self.GetBranch(), prop))
1263 for setting in config_settings:
1264 RunGit(['config', '--unset', setting], error_ok=True)
[email protected]1033efd2013-07-23 23:25:091265 self.issue = None
[email protected]9b7fd712016-06-01 13:45:201266 self.patchset = None
[email protected]cc51cd02010-12-23 00:48:391267
[email protected]15169952011-09-27 14:30:531268 def GetChange(self, upstream_branch, author):
[email protected]0f58fa82012-11-05 01:45:201269 if not self.GitSanityChecks(upstream_branch):
1270 DieWithError('\nGit sanity check failure')
1271
[email protected]8b0553c2014-02-11 00:33:371272 root = settings.GetRelativeRoot()
[email protected]f267b0e2013-05-02 09:11:431273 if not root:
1274 root = '.'
[email protected]512f1ef2011-04-20 15:17:571275 absroot = os.path.abspath(root)
[email protected]6fb99c62011-04-18 15:57:281276
1277 # We use the sha1 of HEAD as a name of this change.
[email protected]8b0553c2014-02-11 00:33:371278 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
[email protected]512f1ef2011-04-20 15:17:571279 # Need to pass a relative path for msysgit.
[email protected]2b38e9c2011-10-19 00:04:351280 try:
[email protected]80a9ef12011-12-13 20:44:101281 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
[email protected]2b38e9c2011-10-19 00:04:351282 except subprocess2.CalledProcessError:
1283 DieWithError(
[email protected]d6617f32013-11-19 00:34:541284 ('\nFailed to diff against upstream branch %s\n\n'
[email protected]2b38e9c2011-10-19 00:04:351285 'This branch probably doesn\'t exist anymore. To reset the\n'
1286 'tracking branch, please run\n'
1287 ' git branch --set-upstream %s trunk\n'
1288 'replacing trunk with origin/master or the relevant branch') %
1289 (upstream_branch, self.GetBranch()))
[email protected]6fb99c62011-04-18 15:57:281290
[email protected]52424302012-08-29 15:14:301291 issue = self.GetIssue()
1292 patchset = self.GetPatchset()
[email protected]6fb99c62011-04-18 15:57:281293 if issue:
1294 description = self.GetDescription()
1295 else:
1296 # If the change was never uploaded, use the log messages of all commits
1297 # up to the branch point, as git cl upload will prefill the description
1298 # with these log messages.
[email protected]8b0553c2014-02-11 00:33:371299 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1300 description = RunGitWithCode(args)[1].strip()
[email protected]03b3bdc2011-06-14 13:04:121301
1302 if not author:
[email protected]13f623c2011-07-22 16:02:231303 author = RunGit(['config', 'user.email']).strip() or None
[email protected]15169952011-09-27 14:30:531304 return presubmit_support.GitChange(
[email protected]6fb99c62011-04-18 15:57:281305 name,
1306 description,
1307 absroot,
1308 files,
1309 issue,
1310 patchset,
[email protected]ea84ef12014-04-30 19:55:121311 author,
1312 upstream=upstream_branch)
[email protected]6fb99c62011-04-18 15:57:281313
[email protected]aa5ced12016-03-29 09:41:141314 def UpdateDescription(self, description):
1315 self.description = description
1316 return self._codereview_impl.UpdateDescriptionRemote(description)
1317
1318 def RunHook(self, committing, may_prompt, verbose, change):
1319 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1320 try:
1321 return presubmit_support.DoPresubmitChecks(change, committing,
1322 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1323 default_presubmit=None, may_prompt=may_prompt,
[email protected]37b07a72016-04-29 16:42:281324 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1325 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
[email protected]aa5ced12016-03-29 09:41:141326 except presubmit_support.PresubmitFailure, e:
1327 DieWithError(
1328 ('%s\nMaybe your depot_tools is out of date?\n'
1329 'If all fails, contact maruel@') % e)
1330
[email protected]f86c7d32016-04-01 19:27:301331 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1332 """Fetches and applies the issue patch from codereview to local branch."""
[email protected]ef7c68c2016-04-07 09:39:391333 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1334 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
[email protected]f86c7d32016-04-01 19:27:301335 else:
1336 # Assume url.
1337 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1338 urlparse.urlparse(issue_arg))
1339 if not parsed_issue_arg or not parsed_issue_arg.valid:
1340 DieWithError('Failed to parse issue argument "%s". '
1341 'Must be an issue number or a valid URL.' % issue_arg)
1342 return self._codereview_impl.CMDPatchWithParsedIssue(
1343 parsed_issue_arg, reject, nocommit, directory)
1344
[email protected]9e6c3a52016-04-12 14:13:081345 def CMDUpload(self, options, git_diff_args, orig_args):
1346 """Uploads a change to codereview."""
1347 if git_diff_args:
1348 # TODO(ukai): is it ok for gerrit case?
1349 base_branch = git_diff_args[0]
1350 else:
1351 if self.GetBranch() is None:
1352 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1353
1354 # Default to diffing against common ancestor of upstream branch
1355 base_branch = self.GetCommonAncestorWithUpstream()
1356 git_diff_args = [base_branch, 'HEAD']
1357
1358 # Make sure authenticated to codereview before running potentially expensive
1359 # hooks. It is a fast, best efforts check. Codereview still can reject the
1360 # authentication during the actual upload.
[email protected]fe30f182016-04-13 12:15:041361 self._codereview_impl.EnsureAuthenticated(force=options.force)
[email protected]9e6c3a52016-04-12 14:13:081362
1363 # Apply watchlists on upload.
1364 change = self.GetChange(base_branch, None)
1365 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1366 files = [f.LocalPath() for f in change.AffectedFiles()]
1367 if not options.bypass_watchlists:
1368 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1369
1370 if not options.bypass_hooks:
1371 if options.reviewers or options.tbr_owners:
1372 # Set the reviewer list now so that presubmit checks can access it.
1373 change_description = ChangeDescription(change.FullDescriptionText())
1374 change_description.update_reviewers(options.reviewers,
1375 options.tbr_owners,
1376 change)
1377 change.SetDescriptionText(change_description.description)
1378 hook_results = self.RunHook(committing=False,
1379 may_prompt=not options.force,
1380 verbose=options.verbose,
1381 change=change)
1382 if not hook_results.should_continue():
1383 return 1
1384 if not options.reviewers and hook_results.reviewers:
1385 options.reviewers = hook_results.reviewers.split(',')
1386
1387 if self.GetIssue():
1388 latest_patchset = self.GetMostRecentPatchset()
1389 local_patchset = self.GetPatchset()
1390 if (latest_patchset and local_patchset and
1391 local_patchset != latest_patchset):
1392 print ('The last upload made from this repository was patchset #%d but '
1393 'the most recent patchset on the server is #%d.'
1394 % (local_patchset, latest_patchset))
1395 print ('Uploading will still work, but if you\'ve uploaded to this '
1396 'issue from another machine or branch the patch you\'re '
1397 'uploading now might not include those changes.')
1398 ask_for_data('About to upload; enter to confirm.')
1399
1400 print_stats(options.similarity, options.find_copies, git_diff_args)
1401 ret = self.CMDUploadChange(options, git_diff_args, change)
1402 if not ret:
1403 git_set_branch_value('last-upload-hash',
1404 RunGit(['rev-parse', 'HEAD']).strip())
1405 # Run post upload hooks, if specified.
1406 if settings.GetRunPostUploadHook():
1407 presubmit_support.DoPostUploadExecuter(
1408 change,
1409 self,
1410 settings.GetRoot(),
1411 options.verbose,
1412 sys.stdout)
1413
1414 # Upload all dependencies if specified.
1415 if options.dependencies:
1416 print
1417 print '--dependencies has been specified.'
1418 print 'All dependent local branches will be re-uploaded.'
1419 print
1420 # Remove the dependencies flag from args so that we do not end up in a
1421 # loop.
1422 orig_args.remove('--dependencies')
1423 ret = upload_branch_deps(self, orig_args)
1424 return ret
1425
[email protected]fa330e82016-04-13 17:09:521426 def SetCQState(self, new_state):
1427 """Update the CQ state for latest patchset.
1428
1429 Issue must have been already uploaded and known.
1430 """
1431 assert new_state in _CQState.ALL_STATES
1432 assert self.GetIssue()
1433 return self._codereview_impl.SetCQState(new_state)
1434
[email protected]aa5ced12016-03-29 09:41:141435 # Forward methods to codereview specific implementation.
1436
1437 def CloseIssue(self):
1438 return self._codereview_impl.CloseIssue()
1439
1440 def GetStatus(self):
1441 return self._codereview_impl.GetStatus()
1442
1443 def GetCodereviewServer(self):
1444 return self._codereview_impl.GetCodereviewServer()
1445
1446 def GetApprovingReviewers(self):
1447 return self._codereview_impl.GetApprovingReviewers()
1448
1449 def GetMostRecentPatchset(self):
1450 return self._codereview_impl.GetMostRecentPatchset()
1451
1452 def __getattr__(self, attr):
1453 # This is because lots of untested code accesses Rietveld-specific stuff
1454 # directly, and it's hard to fix for sure. So, just let it work, and fix
[email protected]cbd7dc32016-05-31 10:33:501455 # on a case by case basis.
[email protected]aa5ced12016-03-29 09:41:141456 return getattr(self._codereview_impl, attr)
1457
1458
1459class _ChangelistCodereviewBase(object):
1460 """Abstract base class encapsulating codereview specifics of a changelist."""
1461 def __init__(self, changelist):
1462 self._changelist = changelist # instance of Changelist
1463
1464 def __getattr__(self, attr):
1465 # Forward methods to changelist.
1466 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1467 # _RietveldChangelistImpl to avoid this hack?
1468 return getattr(self._changelist, attr)
1469
1470 def GetStatus(self):
1471 """Apply a rough heuristic to give a simple summary of an issue's review
1472 or CQ status, assuming adherence to a common workflow.
1473
1474 Returns None if no issue for this branch, or specific string keywords.
1475 """
1476 raise NotImplementedError()
1477
1478 def GetCodereviewServer(self):
1479 """Returns server URL without end slash, like "https://codereview.com"."""
1480 raise NotImplementedError()
1481
1482 def FetchDescription(self):
1483 """Fetches and returns description from the codereview server."""
1484 raise NotImplementedError()
1485
1486 def GetCodereviewServerSetting(self):
1487 """Returns git config setting for the codereview server."""
1488 raise NotImplementedError()
1489
[email protected]5df290f2016-04-11 16:12:291490 @classmethod
1491 def IssueSetting(cls, branch):
[email protected]d03bc632016-04-12 14:17:261492 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
[email protected]5df290f2016-04-11 16:12:291493
1494 @classmethod
[email protected]d03bc632016-04-12 14:17:261495 def IssueSettingSuffix(cls):
[email protected]aa5ced12016-03-29 09:41:141496 """Returns name of git config setting which stores issue number for a given
1497 branch."""
1498 raise NotImplementedError()
1499
1500 def PatchsetSetting(self):
1501 """Returns name of git config setting which stores issue number."""
1502 raise NotImplementedError()
1503
[email protected]9b7fd712016-06-01 13:45:201504 def _PostUnsetIssueProperties(self):
1505 """Which branch-specific properties to erase when unsettin issue."""
1506 raise NotImplementedError()
1507
[email protected]aa5ced12016-03-29 09:41:141508 def GetRieveldObjForPresubmit(self):
1509 # This is an unfortunate Rietveld-embeddedness in presubmit.
1510 # For non-Rietveld codereviews, this probably should return a dummy object.
1511 raise NotImplementedError()
1512
[email protected]37b07a72016-04-29 16:42:281513 def GetGerritObjForPresubmit(self):
1514 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1515 return None
1516
[email protected]aa5ced12016-03-29 09:41:141517 def UpdateDescriptionRemote(self, description):
1518 """Update the description on codereview site."""
1519 raise NotImplementedError()
1520
1521 def CloseIssue(self):
1522 """Closes the issue."""
1523 raise NotImplementedError()
1524
1525 def GetApprovingReviewers(self):
1526 """Returns a list of reviewers approving the change.
1527
1528 Note: not necessarily committers.
1529 """
1530 raise NotImplementedError()
1531
1532 def GetMostRecentPatchset(self):
1533 """Returns the most recent patchset number from the codereview site."""
1534 raise NotImplementedError()
1535
[email protected]f86c7d32016-04-01 19:27:301536 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1537 directory):
1538 """Fetches and applies the issue.
1539
1540 Arguments:
1541 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1542 reject: if True, reject the failed patch instead of switching to 3-way
1543 merge. Rietveld only.
1544 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1545 only.
1546 directory: switch to directory before applying the patch. Rietveld only.
1547 """
1548 raise NotImplementedError()
1549
1550 @staticmethod
1551 def ParseIssueURL(parsed_url):
1552 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1553 failed."""
1554 raise NotImplementedError()
1555
[email protected]fe30f182016-04-13 12:15:041556 def EnsureAuthenticated(self, force):
1557 """Best effort check that user is authenticated with codereview server.
1558
1559 Arguments:
1560 force: whether to skip confirmation questions.
1561 """
[email protected]9e6c3a52016-04-12 14:13:081562 raise NotImplementedError()
1563
[email protected]aa6235b2016-04-11 21:35:291564 def CMDUploadChange(self, options, args, change):
1565 """Uploads a change to codereview."""
1566 raise NotImplementedError()
1567
[email protected]fa330e82016-04-13 17:09:521568 def SetCQState(self, new_state):
1569 """Update the CQ state for latest patchset.
1570
1571 Issue must have been already uploaded and known.
1572 """
1573 raise NotImplementedError()
1574
[email protected]aa5ced12016-03-29 09:41:141575
1576class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1577 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1578 super(_RietveldChangelistImpl, self).__init__(changelist)
1579 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1580 settings.GetDefaultServerUrl()
1581
1582 self._rietveld_server = rietveld_server
1583 self._auth_config = auth_config
1584 self._props = None
1585 self._rpc_server = None
1586
[email protected]aa5ced12016-03-29 09:41:141587 def GetCodereviewServer(self):
1588 if not self._rietveld_server:
1589 # If we're on a branch then get the server potentially associated
1590 # with that branch.
1591 if self.GetIssue():
1592 rietveld_server_setting = self.GetCodereviewServerSetting()
1593 if rietveld_server_setting:
1594 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1595 ['config', rietveld_server_setting], error_ok=True).strip())
1596 if not self._rietveld_server:
1597 self._rietveld_server = settings.GetDefaultServerUrl()
1598 return self._rietveld_server
1599
[email protected]fe30f182016-04-13 12:15:041600 def EnsureAuthenticated(self, force):
[email protected]9e6c3a52016-04-12 14:13:081601 """Best effort check that user is authenticated with Rietveld server."""
1602 if self._auth_config.use_oauth2:
1603 authenticator = auth.get_authenticator_for_host(
1604 self.GetCodereviewServer(), self._auth_config)
1605 if not authenticator.has_cached_credentials():
1606 raise auth.LoginRequiredError(self.GetCodereviewServer())
1607
[email protected]aa5ced12016-03-29 09:41:141608 def FetchDescription(self):
1609 issue = self.GetIssue()
1610 assert issue
1611 try:
1612 return self.RpcServer().get_description(issue).strip()
1613 except urllib2.HTTPError as e:
1614 if e.code == 404:
1615 DieWithError(
1616 ('\nWhile fetching the description for issue %d, received a '
1617 '404 (not found)\n'
1618 'error. It is likely that you deleted this '
1619 'issue on the server. If this is the\n'
1620 'case, please run\n\n'
1621 ' git cl issue 0\n\n'
1622 'to clear the association with the deleted issue. Then run '
1623 'this command again.') % issue)
1624 else:
1625 DieWithError(
1626 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1627 except urllib2.URLError as e:
1628 print >> sys.stderr, (
1629 'Warning: Failed to retrieve CL description due to network '
1630 'failure.')
1631 return ''
1632
1633 def GetMostRecentPatchset(self):
1634 return self.GetIssueProperties()['patchsets'][-1]
1635
1636 def GetPatchSetDiff(self, issue, patchset):
1637 return self.RpcServer().get(
1638 '/download/issue%s_%s.diff' % (issue, patchset))
1639
1640 def GetIssueProperties(self):
1641 if self._props is None:
1642 issue = self.GetIssue()
1643 if not issue:
1644 self._props = {}
1645 else:
1646 self._props = self.RpcServer().get_issue_properties(issue, True)
1647 return self._props
1648
1649 def GetApprovingReviewers(self):
1650 return get_approving_reviewers(self.GetIssueProperties())
1651
1652 def AddComment(self, message):
1653 return self.RpcServer().add_comment(self.GetIssue(), message)
1654
[email protected]b99fbd92014-09-11 17:29:281655 def GetStatus(self):
1656 """Apply a rough heuristic to give a simple summary of an issue's review
1657 or CQ status, assuming adherence to a common workflow.
1658
1659 Returns None if no issue for this branch, or one of the following keywords:
1660 * 'error' - error from review tool (including deleted issues)
1661 * 'unsent' - not sent for review
1662 * 'waiting' - waiting for review
1663 * 'reply' - waiting for owner to reply to review
1664 * 'lgtm' - LGTM from at least one approved reviewer
1665 * 'commit' - in the commit queue
1666 * 'closed' - closed
1667 """
1668 if not self.GetIssue():
1669 return None
1670
1671 try:
1672 props = self.GetIssueProperties()
1673 except urllib2.HTTPError:
1674 return 'error'
1675
1676 if props.get('closed'):
1677 # Issue is closed.
1678 return 'closed'
[email protected]b4f6a222016-03-03 01:11:041679 if props.get('commit') and not props.get('cq_dry_run', False):
[email protected]b99fbd92014-09-11 17:29:281680 # Issue is in the commit queue.
1681 return 'commit'
1682
1683 try:
1684 reviewers = self.GetApprovingReviewers()
1685 except urllib2.HTTPError:
1686 return 'error'
1687
1688 if reviewers:
1689 # Was LGTM'ed.
1690 return 'lgtm'
1691
1692 messages = props.get('messages') or []
1693
1694 if not messages:
1695 # No message was sent.
1696 return 'unsent'
1697 if messages[-1]['sender'] != props.get('owner_email'):
1698 # Non-LGTM reply from non-owner
1699 return 'reply'
1700 return 'waiting'
1701
[email protected]aa5ced12016-03-29 09:41:141702 def UpdateDescriptionRemote(self, description):
[email protected]b021b322013-04-08 17:57:291703 return self.RpcServer().update_description(
1704 self.GetIssue(), self.description)
1705
[email protected]cc51cd02010-12-23 00:48:391706 def CloseIssue(self):
[email protected]b021b322013-04-08 17:57:291707 return self.RpcServer().close_issue(self.GetIssue())
[email protected]cc51cd02010-12-23 00:48:391708
[email protected]27bb3872011-05-30 20:33:191709 def SetFlag(self, flag, value):
1710 """Patchset must match."""
1711 if not self.GetPatchset():
1712 DieWithError('The patchset needs to match. Send another patchset.')
1713 try:
1714 return self.RpcServer().set_flag(
[email protected]52424302012-08-29 15:14:301715 self.GetIssue(), self.GetPatchset(), flag, value)
[email protected]27bb3872011-05-30 20:33:191716 except urllib2.HTTPError, e:
1717 if e.code == 404:
1718 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1719 if e.code == 403:
1720 DieWithError(
1721 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1722 'match?') % (self.GetIssue(), self.GetPatchset()))
1723 raise
[email protected]cc51cd02010-12-23 00:48:391724
[email protected]cab38e92011-04-09 00:30:511725 def RpcServer(self):
[email protected]cc51cd02010-12-23 00:48:391726 """Returns an upload.RpcServer() to access this review's rietveld instance.
1727 """
[email protected]e77ebbf2011-03-29 20:35:381728 if not self._rpc_server:
[email protected]4bac4b52012-11-27 20:33:521729 self._rpc_server = rietveld.CachingRietveld(
[email protected]aa5ced12016-03-29 09:41:141730 self.GetCodereviewServer(),
[email protected]cf6a5d22015-04-09 22:02:001731 self._auth_config or auth.make_auth_config())
[email protected]e77ebbf2011-03-29 20:35:381732 return self._rpc_server
[email protected]cc51cd02010-12-23 00:48:391733
[email protected]5df290f2016-04-11 16:12:291734 @classmethod
[email protected]d03bc632016-04-12 14:17:261735 def IssueSettingSuffix(cls):
[email protected]5df290f2016-04-11 16:12:291736 return 'rietveldissue'
[email protected]cc51cd02010-12-23 00:48:391737
[email protected]aa5ced12016-03-29 09:41:141738 def PatchsetSetting(self):
[email protected]cc51cd02010-12-23 00:48:391739 """Return the git setting that stores this change's most recent patchset."""
1740 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1741
[email protected]aa5ced12016-03-29 09:41:141742 def GetCodereviewServerSetting(self):
[email protected]cc51cd02010-12-23 00:48:391743 """Returns the git setting that stores this change's rietveld server."""
[email protected]d62c61f2014-10-20 22:33:211744 branch = self.GetBranch()
1745 if branch:
1746 return 'branch.%s.rietveldserver' % branch
1747 return None
[email protected]cc51cd02010-12-23 00:48:391748
[email protected]9b7fd712016-06-01 13:45:201749 def _PostUnsetIssueProperties(self):
1750 """Which branch-specific properties to erase when unsetting issue."""
1751 return ['rietveldserver']
1752
[email protected]aa5ced12016-03-29 09:41:141753 def GetRieveldObjForPresubmit(self):
1754 return self.RpcServer()
1755
[email protected]fa330e82016-04-13 17:09:521756 def SetCQState(self, new_state):
1757 props = self.GetIssueProperties()
1758 if props.get('private'):
1759 DieWithError('Cannot set-commit on private issue')
1760
1761 if new_state == _CQState.COMMIT:
1762 self.SetFlag('commit', '1')
1763 elif new_state == _CQState.NONE:
1764 self.SetFlag('commit', '0')
1765 else:
1766 raise NotImplementedError()
1767
1768
[email protected]f86c7d32016-04-01 19:27:301769 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1770 directory):
1771 # TODO(maruel): Use apply_issue.py
1772
1773 # PatchIssue should never be called with a dirty tree. It is up to the
1774 # caller to check this, but just in case we assert here since the
1775 # consequences of the caller not checking this could be dire.
1776 assert(not git_common.is_dirty_git_tree('apply'))
1777 assert(parsed_issue_arg.valid)
1778 self._changelist.issue = parsed_issue_arg.issue
1779 if parsed_issue_arg.hostname:
1780 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1781
[email protected]ef7c68c2016-04-07 09:39:391782 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1783 parsed_issue_arg.patch_url):
[email protected]f86c7d32016-04-01 19:27:301784 assert parsed_issue_arg.patchset
1785 patchset = parsed_issue_arg.patchset
1786 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1787 else:
1788 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1789 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1790
1791 # Switch up to the top-level directory, if necessary, in preparation for
1792 # applying the patch.
1793 top = settings.GetRelativeRoot()
1794 if top:
1795 os.chdir(top)
1796
1797 # Git patches have a/ at the beginning of source paths. We strip that out
1798 # with a sed script rather than the -p flag to patch so we can feed either
1799 # Git or svn-style patches into the same apply command.
1800 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1801 try:
1802 patch_data = subprocess2.check_output(
1803 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1804 except subprocess2.CalledProcessError:
1805 DieWithError('Git patch mungling failed.')
1806 logging.info(patch_data)
1807
1808 # We use "git apply" to apply the patch instead of "patch" so that we can
1809 # pick up file adds.
1810 # The --index flag means: also insert into the index (so we catch adds).
1811 cmd = ['git', 'apply', '--index', '-p0']
1812 if directory:
1813 cmd.extend(('--directory', directory))
1814 if reject:
1815 cmd.append('--reject')
1816 elif IsGitVersionAtLeast('1.7.12'):
1817 cmd.append('--3way')
1818 try:
1819 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1820 stdin=patch_data, stdout=subprocess2.VOID)
1821 except subprocess2.CalledProcessError:
1822 print 'Failed to apply the patch'
1823 return 1
1824
1825 # If we had an issue, commit the current state and register the issue.
1826 if not nocommit:
1827 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1828 'patch from issue %(i)s at patchset '
1829 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1830 % {'i': self.GetIssue(), 'p': patchset})])
1831 self.SetIssue(self.GetIssue())
1832 self.SetPatchset(patchset)
1833 print "Committed patch locally."
1834 else:
1835 print "Patch applied to index."
1836 return 0
1837
1838 @staticmethod
1839 def ParseIssueURL(parsed_url):
1840 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1841 return None
1842 # Typical url: https://domain/<issue_number>[/[other]]
1843 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1844 if match:
1845 return _RietveldParsedIssueNumberArgument(
1846 issue=int(match.group(1)),
1847 hostname=parsed_url.netloc)
1848 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1849 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1850 if match:
1851 return _RietveldParsedIssueNumberArgument(
1852 issue=int(match.group(1)),
1853 patchset=int(match.group(2)),
1854 hostname=parsed_url.netloc,
1855 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1856 return None
1857
[email protected]aa6235b2016-04-11 21:35:291858 def CMDUploadChange(self, options, args, change):
1859 """Upload the patch to Rietveld."""
1860 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1861 upload_args.extend(['--server', self.GetCodereviewServer()])
[email protected]aa6235b2016-04-11 21:35:291862 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1863 if options.emulate_svn_auto_props:
1864 upload_args.append('--emulate_svn_auto_props')
1865
1866 change_desc = None
1867
1868 if options.email is not None:
1869 upload_args.extend(['--email', options.email])
1870
1871 if self.GetIssue():
1872 if options.title:
1873 upload_args.extend(['--title', options.title])
1874 if options.message:
1875 upload_args.extend(['--message', options.message])
1876 upload_args.extend(['--issue', str(self.GetIssue())])
1877 print ('This branch is associated with issue %s. '
1878 'Adding patch to that issue.' % self.GetIssue())
1879 else:
1880 if options.title:
1881 upload_args.extend(['--title', options.title])
1882 message = (options.title or options.message or
1883 CreateDescriptionFromLog(args))
1884 change_desc = ChangeDescription(message)
1885 if options.reviewers or options.tbr_owners:
1886 change_desc.update_reviewers(options.reviewers,
1887 options.tbr_owners,
1888 change)
1889 if not options.force:
1890 change_desc.prompt()
1891
1892 if not change_desc.description:
1893 print "Description is empty; aborting."
1894 return 1
1895
1896 upload_args.extend(['--message', change_desc.description])
1897 if change_desc.get_reviewers():
1898 upload_args.append('--reviewers=%s' % ','.join(
1899 change_desc.get_reviewers()))
1900 if options.send_mail:
1901 if not change_desc.get_reviewers():
1902 DieWithError("Must specify reviewers to send email.")
1903 upload_args.append('--send_mail')
1904
1905 # We check this before applying rietveld.private assuming that in
1906 # rietveld.cc only addresses which we can send private CLs to are listed
1907 # if rietveld.private is set, and so we should ignore rietveld.cc only
1908 # when --private is specified explicitly on the command line.
1909 if options.private:
1910 logging.warn('rietveld.cc is ignored since private flag is specified. '
1911 'You need to review and add them manually if necessary.')
1912 cc = self.GetCCListWithoutDefault()
1913 else:
1914 cc = self.GetCCList()
1915 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1916 if cc:
1917 upload_args.extend(['--cc', cc])
1918
1919 if options.private or settings.GetDefaultPrivateFlag() == "True":
1920 upload_args.append('--private')
1921
1922 upload_args.extend(['--git_similarity', str(options.similarity)])
1923 if not options.find_copies:
1924 upload_args.extend(['--git_no_find_copies'])
1925
1926 # Include the upstream repo's URL in the change -- this is useful for
1927 # projects that have their source spread across multiple repos.
1928 remote_url = self.GetGitBaseUrlFromConfig()
1929 if not remote_url:
1930 if settings.GetIsGitSvn():
1931 remote_url = self.GetGitSvnRemoteUrl()
1932 else:
1933 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1934 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1935 self.GetUpstreamBranch().split('/')[-1])
1936 if remote_url:
1937 upload_args.extend(['--base_url', remote_url])
1938 remote, remote_branch = self.GetRemoteBranch()
1939 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1940 settings.GetPendingRefPrefix())
1941 if target_ref:
1942 upload_args.extend(['--target_ref', target_ref])
1943
1944 # Look for dependent patchsets. See crbug.com/480453 for more details.
1945 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1946 upstream_branch = ShortBranchName(upstream_branch)
1947 if remote is '.':
1948 # A local branch is being tracked.
[email protected]cbd7dc32016-05-31 10:33:501949 local_branch = upstream_branch
[email protected]aa6235b2016-04-11 21:35:291950 if settings.GetIsSkipDependencyUpload(local_branch):
1951 print
1952 print ('Skipping dependency patchset upload because git config '
1953 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1954 print
1955 else:
1956 auth_config = auth.extract_auth_config_from_options(options)
[email protected]cbd7dc32016-05-31 10:33:501957 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
[email protected]aa6235b2016-04-11 21:35:291958 auth_config=auth_config)
1959 branch_cl_issue_url = branch_cl.GetIssueURL()
1960 branch_cl_issue = branch_cl.GetIssue()
1961 branch_cl_patchset = branch_cl.GetPatchset()
1962 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1963 upload_args.extend(
1964 ['--depends_on_patchset', '%s:%s' % (
1965 branch_cl_issue, branch_cl_patchset)])
[email protected]9e6c3a52016-04-12 14:13:081966 print(
[email protected]aa6235b2016-04-11 21:35:291967 '\n'
1968 'The current branch (%s) is tracking a local branch (%s) with '
1969 'an associated CL.\n'
1970 'Adding %s/#ps%s as a dependency patchset.\n'
1971 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1972 branch_cl_patchset))
1973
1974 project = settings.GetProject()
1975 if project:
1976 upload_args.extend(['--project', project])
1977
1978 if options.cq_dry_run:
1979 upload_args.extend(['--cq_dry_run'])
1980
1981 try:
1982 upload_args = ['upload'] + upload_args + args
1983 logging.info('upload.RealMain(%s)', upload_args)
1984 issue, patchset = upload.RealMain(upload_args)
1985 issue = int(issue)
1986 patchset = int(patchset)
1987 except KeyboardInterrupt:
1988 sys.exit(1)
1989 except:
1990 # If we got an exception after the user typed a description for their
1991 # change, back up the description before re-raising.
1992 if change_desc:
1993 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1994 print('\nGot exception while uploading -- saving description to %s\n' %
1995 backup_path)
1996 backup_file = open(backup_path, 'w')
1997 backup_file.write(change_desc.description)
1998 backup_file.close()
1999 raise
2000
2001 if not self.GetIssue():
2002 self.SetIssue(issue)
2003 self.SetPatchset(patchset)
2004
2005 if options.use_commit_queue:
[email protected]fa330e82016-04-13 17:09:522006 self.SetCQState(_CQState.COMMIT)
[email protected]aa6235b2016-04-11 21:35:292007 return 0
2008
[email protected]aa5ced12016-03-29 09:41:142009
2010class _GerritChangelistImpl(_ChangelistCodereviewBase):
2011 def __init__(self, changelist, auth_config=None):
2012 # auth_config is Rietveld thing, kept here to preserve interface only.
2013 super(_GerritChangelistImpl, self).__init__(changelist)
2014 self._change_id = None
[email protected]fe30f182016-04-13 12:15:042015 # Lazily cached values.
[email protected]aa5ced12016-03-29 09:41:142016 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
[email protected]fe30f182016-04-13 12:15:042017 self._gerrit_host = None # e.g. chromium-review.googlesource.com
[email protected]aa5ced12016-03-29 09:41:142018
2019 def _GetGerritHost(self):
2020 # Lazy load of configs.
2021 self.GetCodereviewServer()
2022 return self._gerrit_host
2023
[email protected]fe30f182016-04-13 12:15:042024 def _GetGitHost(self):
2025 """Returns git host to be used when uploading change to Gerrit."""
2026 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2027
[email protected]aa5ced12016-03-29 09:41:142028 def GetCodereviewServer(self):
2029 if not self._gerrit_server:
2030 # If we're on a branch then get the server potentially associated
2031 # with that branch.
2032 if self.GetIssue():
2033 gerrit_server_setting = self.GetCodereviewServerSetting()
2034 if gerrit_server_setting:
2035 self._gerrit_server = RunGit(['config', gerrit_server_setting],
2036 error_ok=True).strip()
2037 if self._gerrit_server:
2038 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
2039 if not self._gerrit_server:
2040 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2041 # has "-review" suffix for lowest level subdomain.
[email protected]fe30f182016-04-13 12:15:042042 parts = self._GetGitHost().split('.')
[email protected]aa5ced12016-03-29 09:41:142043 parts[0] = parts[0] + '-review'
2044 self._gerrit_host = '.'.join(parts)
2045 self._gerrit_server = 'https://%s' % self._gerrit_host
2046 return self._gerrit_server
2047
[email protected]5df290f2016-04-11 16:12:292048 @classmethod
[email protected]d03bc632016-04-12 14:17:262049 def IssueSettingSuffix(cls):
[email protected]5df290f2016-04-11 16:12:292050 return 'gerritissue'
[email protected]aa5ced12016-03-29 09:41:142051
[email protected]fe30f182016-04-13 12:15:042052 def EnsureAuthenticated(self, force):
[email protected]9e6c3a52016-04-12 14:13:082053 """Best effort check that user is authenticated with Gerrit server."""
[email protected]28253532016-04-14 13:46:562054 if settings.GetGerritSkipEnsureAuthenticated():
2055 # For projects with unusual authentication schemes.
2056 # See http://crbug.com/603378.
2057 return
[email protected]fe30f182016-04-13 12:15:042058 # Lazy-loader to identify Gerrit and Git hosts.
2059 if gerrit_util.GceAuthenticator.is_gce():
2060 return
2061 self.GetCodereviewServer()
2062 git_host = self._GetGitHost()
2063 assert self._gerrit_server and self._gerrit_host
2064 cookie_auth = gerrit_util.CookiesAuthenticator()
2065
2066 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2067 git_auth = cookie_auth.get_auth_header(git_host)
2068 if gerrit_auth and git_auth:
2069 if gerrit_auth == git_auth:
2070 return
2071 print((
2072 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2073 ' Check your %s or %s file for credentials of hosts:\n'
2074 ' %s\n'
2075 ' %s\n'
2076 ' %s') %
2077 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2078 git_host, self._gerrit_host,
2079 cookie_auth.get_new_password_message(git_host)))
2080 if not force:
2081 ask_for_data('If you know what you are doing, press Enter to continue, '
2082 'Ctrl+C to abort.')
2083 return
2084 else:
2085 missing = (
2086 [] if gerrit_auth else [self._gerrit_host] +
2087 [] if git_auth else [git_host])
2088 DieWithError('Credentials for the following hosts are required:\n'
2089 ' %s\n'
2090 'These are read from %s (or legacy %s)\n'
2091 '%s' % (
2092 '\n '.join(missing),
2093 cookie_auth.get_gitcookies_path(),
2094 cookie_auth.get_netrc_path(),
2095 cookie_auth.get_new_password_message(git_host)))
2096
[email protected]9e6c3a52016-04-12 14:13:082097
[email protected]aa5ced12016-03-29 09:41:142098 def PatchsetSetting(self):
2099 """Return the git setting that stores this change's most recent patchset."""
2100 return 'branch.%s.gerritpatchset' % self.GetBranch()
2101
2102 def GetCodereviewServerSetting(self):
2103 """Returns the git setting that stores this change's Gerrit server."""
2104 branch = self.GetBranch()
2105 if branch:
2106 return 'branch.%s.gerritserver' % branch
2107 return None
2108
[email protected]9b7fd712016-06-01 13:45:202109 def _PostUnsetIssueProperties(self):
2110 """Which branch-specific properties to erase when unsetting issue."""
2111 return [
2112 'gerritserver',
2113 'gerritsquashhash',
2114 ]
2115
[email protected]aa5ced12016-03-29 09:41:142116 def GetRieveldObjForPresubmit(self):
2117 class ThisIsNotRietveldIssue(object):
2118 def __nonzero__(self):
2119 # This is a hack to make presubmit_support think that rietveld is not
2120 # defined, yet still ensure that calls directly result in a decent
2121 # exception message below.
2122 return False
2123
2124 def __getattr__(self, attr):
2125 print(
2126 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2127 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2128 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2129 'or use Rietveld for codereview.\n'
2130 'See also http://crbug.com/579160.' % attr)
2131 raise NotImplementedError()
2132 return ThisIsNotRietveldIssue()
2133
[email protected]37b07a72016-04-29 16:42:282134 def GetGerritObjForPresubmit(self):
2135 return presubmit_support.GerritAccessor(self._GetGerritHost())
2136
[email protected]aa5ced12016-03-29 09:41:142137 def GetStatus(self):
[email protected]013a2802016-03-29 09:52:332138 """Apply a rough heuristic to give a simple summary of an issue's review
2139 or CQ status, assuming adherence to a common workflow.
2140
2141 Returns None if no issue for this branch, or one of the following keywords:
2142 * 'error' - error from review tool (including deleted issues)
2143 * 'unsent' - no reviewers added
2144 * 'waiting' - waiting for review
2145 * 'reply' - waiting for owner to reply to review
2146 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2147 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2148 * 'commit' - in the commit queue
2149 * 'closed' - abandoned
2150 """
2151 if not self.GetIssue():
2152 return None
2153
2154 try:
2155 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2156 except httplib.HTTPException:
2157 return 'error'
2158
[email protected]5e1bf382016-05-17 08:43:242159 if data['status'] in ('ABANDONED', 'MERGED'):
[email protected]013a2802016-03-29 09:52:332160 return 'closed'
2161
2162 cq_label = data['labels'].get('Commit-Queue', {})
2163 if cq_label:
2164 # Vote value is a stringified integer, which we expect from 0 to 2.
2165 vote_value = cq_label.get('value', '0')
2166 vote_text = cq_label.get('values', {}).get(vote_value, '')
2167 if vote_text.lower() == 'commit':
2168 return 'commit'
2169
2170 lgtm_label = data['labels'].get('Code-Review', {})
2171 if lgtm_label:
2172 if 'rejected' in lgtm_label:
2173 return 'not lgtm'
2174 if 'approved' in lgtm_label:
2175 return 'lgtm'
2176
2177 if not data.get('reviewers', {}).get('REVIEWER', []):
2178 return 'unsent'
2179
2180 messages = data.get('messages', [])
2181 if messages:
2182 owner = data['owner'].get('_account_id')
2183 last_message_author = messages[-1].get('author', {}).get('_account_id')
2184 if owner != last_message_author:
2185 # Some reply from non-owner.
2186 return 'reply'
2187
2188 return 'waiting'
[email protected]aa5ced12016-03-29 09:41:142189
2190 def GetMostRecentPatchset(self):
[email protected]013a2802016-03-29 09:52:332191 data = self._GetChangeDetail(['CURRENT_REVISION'])
[email protected]aa5ced12016-03-29 09:41:142192 return data['revisions'][data['current_revision']]['_number']
2193
2194 def FetchDescription(self):
[email protected]2d3da632016-04-25 19:23:272195 data = self._GetChangeDetail(['CURRENT_REVISION'])
2196 current_rev = data['current_revision']
2197 url = data['revisions'][current_rev]['fetch']['http']['url']
2198 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
[email protected]aa5ced12016-03-29 09:41:142199
2200 def UpdateDescriptionRemote(self, description):
[email protected]6d1266e2016-04-26 11:12:262201 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2202 description)
[email protected]aa5ced12016-03-29 09:41:142203
2204 def CloseIssue(self):
2205 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2206
[email protected]600b4922016-04-26 10:57:522207 def GetApprovingReviewers(self):
2208 """Returns a list of reviewers approving the change.
2209
2210 Note: not necessarily committers.
2211 """
2212 raise NotImplementedError()
2213
[email protected]d68b62b2016-03-31 16:09:292214 def SubmitIssue(self, wait_for_merge=True):
2215 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2216 wait_for_merge=wait_for_merge)
[email protected]cc51cd02010-12-23 00:48:392217
[email protected]aa6235b2016-04-11 21:35:292218 def _GetChangeDetail(self, options=None, issue=None):
2219 options = options or []
2220 issue = issue or self.GetIssue()
2221 assert issue, 'issue required to query Gerrit'
[email protected]11a899e2016-04-13 12:45:442222 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2223 options)
[email protected]013a2802016-03-29 09:52:332224
[email protected]d68b62b2016-03-31 16:09:292225 def CMDLand(self, force, bypass_hooks, verbose):
2226 if git_common.is_dirty_git_tree('land'):
2227 return 1
2228 differs = True
2229 last_upload = RunGit(['config',
2230 'branch.%s.gerritsquashhash' % self.GetBranch()],
2231 error_ok=True).strip()
2232 # Note: git diff outputs nothing if there is no diff.
2233 if not last_upload or RunGit(['diff', last_upload]).strip():
2234 print('WARNING: some changes from local branch haven\'t been uploaded')
2235 else:
2236 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2237 if detail['current_revision'] == last_upload:
2238 differs = False
2239 else:
2240 print('WARNING: local branch contents differ from latest uploaded '
2241 'patchset')
2242 if differs:
2243 if not force:
2244 ask_for_data(
2245 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2246 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2247 elif not bypass_hooks:
2248 hook_results = self.RunHook(
2249 committing=True,
2250 may_prompt=not force,
2251 verbose=verbose,
2252 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2253 if not hook_results.should_continue():
2254 return 1
2255
2256 self.SubmitIssue(wait_for_merge=True)
2257 print('Issue %s has been submitted.' % self.GetIssueURL())
2258 return 0
2259
[email protected]f86c7d32016-04-01 19:27:302260 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2261 directory):
2262 assert not reject
2263 assert not nocommit
2264 assert not directory
2265 assert parsed_issue_arg.valid
2266
2267 self._changelist.issue = parsed_issue_arg.issue
2268
2269 if parsed_issue_arg.hostname:
2270 self._gerrit_host = parsed_issue_arg.hostname
2271 self._gerrit_server = 'https://%s' % self._gerrit_host
2272
2273 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2274
2275 if not parsed_issue_arg.patchset:
2276 # Use current revision by default.
2277 revision_info = detail['revisions'][detail['current_revision']]
2278 patchset = int(revision_info['_number'])
2279 else:
2280 patchset = parsed_issue_arg.patchset
2281 for revision_info in detail['revisions'].itervalues():
2282 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2283 break
2284 else:
2285 DieWithError('Couldn\'t find patchset %i in issue %i' %
2286 (parsed_issue_arg.patchset, self.GetIssue()))
2287
2288 fetch_info = revision_info['fetch']['http']
2289 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2290 RunGit(['cherry-pick', 'FETCH_HEAD'])
2291 self.SetIssue(self.GetIssue())
2292 self.SetPatchset(patchset)
2293 print('Committed patch for issue %i pathset %i locally' %
2294 (self.GetIssue(), self.GetPatchset()))
2295 return 0
2296
2297 @staticmethod
2298 def ParseIssueURL(parsed_url):
2299 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2300 return None
2301 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2302 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2303 # Short urls like https://domain/<issue_number> can be used, but don't allow
2304 # specifying the patchset (you'd 404), but we allow that here.
2305 if parsed_url.path == '/':
2306 part = parsed_url.fragment
2307 else:
2308 part = parsed_url.path
2309 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2310 if match:
2311 return _ParsedIssueNumberArgument(
2312 issue=int(match.group(2)),
2313 patchset=int(match.group(4)) if match.group(4) else None,
2314 hostname=parsed_url.netloc)
2315 return None
2316
tandrii16e0b4e2016-06-07 17:34:282317 def _GerritCommitMsgHookCheck(self, offer_removal):
2318 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2319 if not os.path.exists(hook):
2320 return
2321 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2322 # custom developer made one.
2323 data = gclient_utils.FileRead(hook)
2324 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2325 return
2326 print('Warning: you have Gerrit commit-msg hook installed.\n'
2327 'It is not neccessary for uploading with git cl in squash mode, '
2328 'and may interfere with it in subtle ways.\n'
2329 'We recommend you remove the commit-msg hook.')
2330 if offer_removal:
2331 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2332 if reply.lower().startswith('y'):
2333 gclient_utils.rm_file_or_tree(hook)
2334 print('Gerrit commit-msg hook removed.')
2335 else:
2336 print('OK, will keep Gerrit commit-msg hook in place.')
2337
[email protected]aa6235b2016-04-11 21:35:292338 def CMDUploadChange(self, options, args, change):
2339 """Upload the current branch to Gerrit."""
[email protected]9e6c3a52016-04-12 14:13:082340 if options.squash and options.no_squash:
2341 DieWithError('Can only use one of --squash or --no-squash')
2342 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2343 not options.no_squash)
[email protected]aa6235b2016-04-11 21:35:292344 # We assume the remote called "origin" is the one we want.
2345 # It is probably not worthwhile to support different workflows.
2346 gerrit_remote = 'origin'
2347
2348 remote, remote_branch = self.GetRemoteBranch()
2349 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2350 pending_prefix='')
2351
[email protected]aa6235b2016-04-11 21:35:292352 if options.squash:
tandrii16e0b4e2016-06-07 17:34:282353 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
[email protected]aa6235b2016-04-11 21:35:292354 if not self.GetIssue():
2355 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2356 # with shadow branch, which used to contain change-id for a given
2357 # branch, using which we can fetch actual issue number and set it as the
2358 # property of the branch, which is the new way.
2359 message = RunGitSilent([
2360 'show', '--format=%B', '-s',
2361 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2362 if message:
2363 change_ids = git_footers.get_footer_change_id(message.strip())
2364 if change_ids and len(change_ids) == 1:
2365 details = self._GetChangeDetail(issue=change_ids[0])
2366 if details:
2367 print('WARNING: found old upload in branch git_cl_uploads/%s '
2368 'corresponding to issue %s' %
2369 (self.GetBranch(), details['_number']))
2370 self.SetIssue(details['_number'])
2371 if not self.GetIssue():
2372 DieWithError(
2373 '\n' # For readability of the blob below.
2374 'Found old upload in branch git_cl_uploads/%s, '
2375 'but failed to find corresponding Gerrit issue.\n'
2376 'If you know the issue number, set it manually first:\n'
2377 ' git cl issue 123456\n'
2378 'If you intended to upload this CL as new issue, '
2379 'just delete or rename the old upload branch:\n'
2380 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2381 'After that, please run git cl upload again.' %
2382 tuple([self.GetBranch()] * 3))
2383 # End of backwards compatability.
2384
2385 if self.GetIssue():
2386 # Try to get the message from a previous upload.
2387 message = self.GetDescription()
2388 if not message:
2389 DieWithError(
2390 'failed to fetch description from current Gerrit issue %d\n'
2391 '%s' % (self.GetIssue(), self.GetIssueURL()))
2392 change_id = self._GetChangeDetail()['change_id']
2393 while True:
2394 footer_change_ids = git_footers.get_footer_change_id(message)
2395 if footer_change_ids == [change_id]:
2396 break
2397 if not footer_change_ids:
2398 message = git_footers.add_footer_change_id(message, change_id)
2399 print('WARNING: appended missing Change-Id to issue description')
2400 continue
2401 # There is already a valid footer but with different or several ids.
2402 # Doing this automatically is non-trivial as we don't want to lose
2403 # existing other footers, yet we want to append just 1 desired
2404 # Change-Id. Thus, just create a new footer, but let user verify the
2405 # new description.
2406 message = '%s\n\nChange-Id: %s' % (message, change_id)
2407 print(
2408 'WARNING: issue %s has Change-Id footer(s):\n'
2409 ' %s\n'
2410 'but issue has Change-Id %s, according to Gerrit.\n'
2411 'Please, check the proposed correction to the description, '
2412 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2413 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2414 change_id))
2415 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2416 if not options.force:
2417 change_desc = ChangeDescription(message)
2418 change_desc.prompt()
2419 message = change_desc.description
2420 if not message:
2421 DieWithError("Description is empty. Aborting...")
2422 # Continue the while loop.
2423 # Sanity check of this code - we should end up with proper message
2424 # footer.
2425 assert [change_id] == git_footers.get_footer_change_id(message)
2426 change_desc = ChangeDescription(message)
2427 else:
2428 change_desc = ChangeDescription(
2429 options.message or CreateDescriptionFromLog(args))
2430 if not options.force:
2431 change_desc.prompt()
2432 if not change_desc.description:
2433 DieWithError("Description is empty. Aborting...")
2434 message = change_desc.description
2435 change_ids = git_footers.get_footer_change_id(message)
2436 if len(change_ids) > 1:
2437 DieWithError('too many Change-Id footers, at most 1 allowed.')
2438 if not change_ids:
2439 # Generate the Change-Id automatically.
2440 message = git_footers.add_footer_change_id(
2441 message, GenerateGerritChangeId(message))
2442 change_desc.set_description(message)
2443 change_ids = git_footers.get_footer_change_id(message)
2444 assert len(change_ids) == 1
2445 change_id = change_ids[0]
2446
2447 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2448 if remote is '.':
2449 # If our upstream branch is local, we base our squashed commit on its
2450 # squashed version.
2451 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2452 # Check the squashed hash of the parent.
2453 parent = RunGit(['config',
2454 'branch.%s.gerritsquashhash' % upstream_branch_name],
2455 error_ok=True).strip()
2456 # Verify that the upstream branch has been uploaded too, otherwise
2457 # Gerrit will create additional CLs when uploading.
2458 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2459 RunGitSilent(['rev-parse', parent + ':'])):
2460 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2461 DieWithError(
2462 'Upload upstream branch %s first.\n'
2463 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2464 'version of depot_tools. If so, then re-upload it with:\n'
2465 ' git cl upload --squash\n' % upstream_branch_name)
2466 else:
2467 parent = self.GetCommonAncestorWithUpstream()
2468
2469 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2470 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2471 '-m', message]).strip()
2472 else:
2473 change_desc = ChangeDescription(
2474 options.message or CreateDescriptionFromLog(args))
2475 if not change_desc.description:
2476 DieWithError("Description is empty. Aborting...")
2477
2478 if not git_footers.get_footer_change_id(change_desc.description):
2479 DownloadGerritHook(False)
[email protected]8930b3d2016-04-13 14:47:022480 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2481 args))
[email protected]aa6235b2016-04-11 21:35:292482 ref_to_push = 'HEAD'
2483 parent = '%s/%s' % (gerrit_remote, branch)
2484 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2485
2486 assert change_desc
2487 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2488 ref_to_push)]).splitlines()
2489 if len(commits) > 1:
2490 print('WARNING: This will upload %d commits. Run the following command '
2491 'to see which commits will be uploaded: ' % len(commits))
2492 print('git log %s..%s' % (parent, ref_to_push))
2493 print('You can also use `git squash-branch` to squash these into a '
2494 'single commit.')
2495 ask_for_data('About to upload; enter to confirm.')
2496
2497 if options.reviewers or options.tbr_owners:
2498 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2499 change)
2500
[email protected]bf766ba2016-04-13 12:51:232501 # Extra options that can be specified at push time. Doc:
2502 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2503 refspec_opts = []
2504 if options.title:
2505 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2506 # reverse on its side.
2507 if '_' in options.title:
2508 print('WARNING: underscores in title will be converted to spaces.')
2509 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2510
[email protected]8da45402016-05-24 23:11:032511 if options.send_mail:
2512 if not change_desc.get_reviewers():
2513 DieWithError('Must specify reviewers to send email.')
2514 refspec_opts.append('notify=ALL')
2515 else:
2516 refspec_opts.append('notify=NONE')
2517
[email protected]aa6235b2016-04-11 21:35:292518 cc = self.GetCCList().split(',')
2519 if options.cc:
2520 cc.extend(options.cc)
2521 cc = filter(None, cc)
2522 if cc:
[email protected]074c2af2016-06-03 23:18:402523 refspec_opts.extend('cc=' + email.strip() for email in cc)
[email protected]aa6235b2016-04-11 21:35:292524
[email protected]8acd8332016-04-13 12:56:032525 if change_desc.get_reviewers():
2526 refspec_opts.extend('r=' + email.strip()
2527 for email in change_desc.get_reviewers())
2528
[email protected]bf766ba2016-04-13 12:51:232529 refspec_suffix = ''
2530 if refspec_opts:
2531 refspec_suffix = '%' + ','.join(refspec_opts)
2532 assert ' ' not in refspec_suffix, (
2533 'spaces not allowed in refspec: "%s"' % refspec_suffix)
[email protected]bf766ba2016-04-13 12:51:232534 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
[email protected]bf766ba2016-04-13 12:51:232535
[email protected]aa6235b2016-04-11 21:35:292536 push_stdout = gclient_utils.CheckCallAndFilter(
[email protected]8acd8332016-04-13 12:56:032537 ['git', 'push', gerrit_remote, refspec],
[email protected]aa6235b2016-04-11 21:35:292538 print_stdout=True,
2539 # Flush after every line: useful for seeing progress when running as
2540 # recipe.
2541 filter_fn=lambda _: sys.stdout.flush())
2542
2543 if options.squash:
2544 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2545 change_numbers = [m.group(1)
2546 for m in map(regex.match, push_stdout.splitlines())
2547 if m]
2548 if len(change_numbers) != 1:
2549 DieWithError(
2550 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2551 'Change-Id: %s') % (len(change_numbers), change_id))
2552 self.SetIssue(change_numbers[0])
2553 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2554 ref_to_push])
2555 return 0
2556
[email protected]8930b3d2016-04-13 14:47:022557 def _AddChangeIdToCommitMessage(self, options, args):
2558 """Re-commits using the current message, assumes the commit hook is in
2559 place.
2560 """
2561 log_desc = options.message or CreateDescriptionFromLog(args)
2562 git_command = ['commit', '--amend', '-m', log_desc]
2563 RunGit(git_command)
2564 new_log_desc = CreateDescriptionFromLog(args)
2565 if git_footers.get_footer_change_id(new_log_desc):
2566 print 'git-cl: Added Change-Id to commit message.'
2567 return new_log_desc
2568 else:
[email protected]b067ec52016-05-31 15:24:442569 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
[email protected]aa6235b2016-04-11 21:35:292570
[email protected]fa330e82016-04-13 17:09:522571 def SetCQState(self, new_state):
2572 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
2573 # TODO(tandrii): maybe allow configurability in codereview.settings or by
2574 # self-discovery of label config for this CL using REST API.
2575 vote_map = {
2576 _CQState.NONE: 0,
2577 _CQState.DRY_RUN: 1,
2578 _CQState.COMMIT : 2,
2579 }
2580 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2581 labels={'Commit-Queue': vote_map[new_state]})
2582
[email protected]d68b62b2016-03-31 16:09:292583
2584_CODEREVIEW_IMPLEMENTATIONS = {
2585 'rietveld': _RietveldChangelistImpl,
2586 'gerrit': _GerritChangelistImpl,
2587}
2588
[email protected]013a2802016-03-29 09:52:332589
[email protected]dde64622016-04-13 17:11:212590def _add_codereview_select_options(parser):
2591 """Appends --gerrit and --rietveld options to force specific codereview."""
2592 parser.codereview_group = optparse.OptionGroup(
2593 parser, 'EXPERIMENTAL! Codereview override options')
2594 parser.add_option_group(parser.codereview_group)
2595 parser.codereview_group.add_option(
2596 '--gerrit', action='store_true',
2597 help='Force the use of Gerrit for codereview')
2598 parser.codereview_group.add_option(
2599 '--rietveld', action='store_true',
2600 help='Force the use of Rietveld for codereview')
2601
2602
2603def _process_codereview_select_options(parser, options):
2604 if options.gerrit and options.rietveld:
2605 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2606 options.forced_codereview = None
2607 if options.gerrit:
2608 options.forced_codereview = 'gerrit'
2609 elif options.rietveld:
2610 options.forced_codereview = 'rietveld'
2611
2612
[email protected]20254fc2011-03-22 18:28:592613class ChangeDescription(object):
2614 """Contains a parsed form of the change description."""
[email protected]c6f60e82013-04-19 17:01:572615 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
[email protected]42c20792013-09-12 17:34:492616 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
[email protected]20254fc2011-03-22 18:28:592617
[email protected]78936cb2013-04-11 00:17:522618 def __init__(self, description):
[email protected]42c20792013-09-12 17:34:492619 self._description_lines = (description or '').strip().splitlines()
[email protected]78936cb2013-04-11 00:17:522620
[email protected]42c20792013-09-12 17:34:492621 @property # www.logilab.org/ticket/89786
2622 def description(self): # pylint: disable=E0202
2623 return '\n'.join(self._description_lines)
2624
2625 def set_description(self, desc):
2626 if isinstance(desc, basestring):
2627 lines = desc.splitlines()
2628 else:
2629 lines = [line.rstrip() for line in desc]
2630 while lines and not lines[0]:
2631 lines.pop(0)
2632 while lines and not lines[-1]:
2633 lines.pop(-1)
2634 self._description_lines = lines
[email protected]78936cb2013-04-11 00:17:522635
[email protected]336f9122014-09-04 02:16:552636 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
[email protected]42c20792013-09-12 17:34:492637 """Rewrites the R=/TBR= line(s) as a single line each."""
[email protected]78936cb2013-04-11 00:17:522638 assert isinstance(reviewers, list), reviewers
[email protected]336f9122014-09-04 02:16:552639 if not reviewers and not add_owners_tbr:
[email protected]78936cb2013-04-11 00:17:522640 return
[email protected]42c20792013-09-12 17:34:492641 reviewers = reviewers[:]
[email protected]78936cb2013-04-11 00:17:522642
[email protected]42c20792013-09-12 17:34:492643 # Get the set of R= and TBR= lines and remove them from the desciption.
2644 regexp = re.compile(self.R_LINE)
2645 matches = [regexp.match(line) for line in self._description_lines]
2646 new_desc = [l for i, l in enumerate(self._description_lines)
2647 if not matches[i]]
2648 self.set_description(new_desc)
[email protected]78936cb2013-04-11 00:17:522649
[email protected]42c20792013-09-12 17:34:492650 # Construct new unified R= and TBR= lines.
2651 r_names = []
2652 tbr_names = []
2653 for match in matches:
2654 if not match:
2655 continue
2656 people = cleanup_list([match.group(2).strip()])
2657 if match.group(1) == 'TBR':
2658 tbr_names.extend(people)
2659 else:
2660 r_names.extend(people)
2661 for name in r_names:
2662 if name not in reviewers:
2663 reviewers.append(name)
[email protected]336f9122014-09-04 02:16:552664 if add_owners_tbr:
2665 owners_db = owners.Database(change.RepositoryRoot(),
2666 fopen=file, os_path=os.path, glob=glob.glob)
2667 all_reviewers = set(tbr_names + reviewers)
2668 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2669 all_reviewers)
2670 tbr_names.extend(owners_db.reviewers_for(missing_files,
2671 change.author_email))
[email protected]42c20792013-09-12 17:34:492672 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2673 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2674
2675 # Put the new lines in the description where the old first R= line was.
2676 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2677 if 0 <= line_loc < len(self._description_lines):
2678 if new_tbr_line:
2679 self._description_lines.insert(line_loc, new_tbr_line)
2680 if new_r_line:
2681 self._description_lines.insert(line_loc, new_r_line)
[email protected]78936cb2013-04-11 00:17:522682 else:
[email protected]42c20792013-09-12 17:34:492683 if new_r_line:
2684 self.append_footer(new_r_line)
2685 if new_tbr_line:
2686 self.append_footer(new_tbr_line)
[email protected]78936cb2013-04-11 00:17:522687
2688 def prompt(self):
2689 """Asks the user to update the description."""
[email protected]42c20792013-09-12 17:34:492690 self.set_description([
2691 '# Enter a description of the change.',
2692 '# This will be displayed on the codereview site.',
2693 '# The first line will also be used as the subject of the review.',
[email protected]bd1073e2013-06-01 00:34:382694 '#--------------------This line is 72 characters long'
[email protected]42c20792013-09-12 17:34:492695 '--------------------',
2696 ] + self._description_lines)
[email protected]78936cb2013-04-11 00:17:522697
[email protected]42c20792013-09-12 17:34:492698 regexp = re.compile(self.BUG_LINE)
2699 if not any((regexp.match(line) for line in self._description_lines)):
[email protected]90752582014-01-14 21:04:502700 self.append_footer('BUG=%s' % settings.GetBugPrefix())
[email protected]42c20792013-09-12 17:34:492701 content = gclient_utils.RunEditor(self.description, True,
[email protected]615a2622013-05-03 13:20:142702 git_editor=settings.GetGitEditor())
[email protected]0e0436a2011-10-25 13:32:412703 if not content:
2704 DieWithError('Running editor failed')
[email protected]42c20792013-09-12 17:34:492705 lines = content.splitlines()
[email protected]78936cb2013-04-11 00:17:522706
2707 # Strip off comments.
[email protected]42c20792013-09-12 17:34:492708 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2709 if not clean_lines:
[email protected]0e0436a2011-10-25 13:32:412710 DieWithError('No CL description, aborting')
[email protected]42c20792013-09-12 17:34:492711 self.set_description(clean_lines)
[email protected]20254fc2011-03-22 18:28:592712
[email protected]78936cb2013-04-11 00:17:522713 def append_footer(self, line):
[email protected]601e1d12016-06-03 13:03:542714 """Adds a footer line to the description.
2715
2716 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2717 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2718 that Gerrit footers are always at the end.
2719 """
2720 parsed_footer_line = git_footers.parse_footer(line)
2721 if parsed_footer_line:
2722 # Line is a gerrit footer in the form: Footer-Key: any value.
2723 # Thus, must be appended observing Gerrit footer rules.
2724 self.set_description(
2725 git_footers.add_footer(self.description,
2726 key=parsed_footer_line[0],
2727 value=parsed_footer_line[1]))
2728 return
2729
2730 if not self._description_lines:
2731 self._description_lines.append(line)
2732 return
2733
2734 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2735 if gerrit_footers:
2736 # git_footers.split_footers ensures that there is an empty line before
2737 # actual (gerrit) footers, if any. We have to keep it that way.
2738 assert top_lines and top_lines[-1] == ''
2739 top_lines, separator = top_lines[:-1], top_lines[-1:]
2740 else:
2741 separator = [] # No need for separator if there are no gerrit_footers.
2742
2743 prev_line = top_lines[-1] if top_lines else ''
2744 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2745 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2746 top_lines.append('')
2747 top_lines.append(line)
2748 self._description_lines = top_lines + separator + gerrit_footers
[email protected]20254fc2011-03-22 18:28:592749
[email protected]78936cb2013-04-11 00:17:522750 def get_reviewers(self):
2751 """Retrieves the list of reviewers."""
[email protected]42c20792013-09-12 17:34:492752 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2753 reviewers = [match.group(2).strip() for match in matches if match]
[email protected]78936cb2013-04-11 00:17:522754 return cleanup_list(reviewers)
[email protected]20254fc2011-03-22 18:28:592755
2756
[email protected]e52678e2013-04-26 18:34:442757def get_approving_reviewers(props):
2758 """Retrieves the reviewers that approved a CL from the issue properties with
2759 messages.
2760
2761 Note that the list may contain reviewers that are not committer, thus are not
2762 considered by the CQ.
2763 """
2764 return sorted(
2765 set(
2766 message['sender']
2767 for message in props['messages']
2768 if message['approval'] and message['sender'] in props['reviewers']
2769 )
2770 )
2771
2772
[email protected]cc51cd02010-12-23 00:48:392773def FindCodereviewSettingsFile(filename='codereview.settings'):
2774 """Finds the given file starting in the cwd and going up.
2775
2776 Only looks up to the top of the repository unless an
2777 'inherit-review-settings-ok' file exists in the root of the repository.
2778 """
2779 inherit_ok_file = 'inherit-review-settings-ok'
2780 cwd = os.getcwd()
[email protected]8b0553c2014-02-11 00:33:372781 root = settings.GetRoot()
[email protected]cc51cd02010-12-23 00:48:392782 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2783 root = '/'
2784 while True:
2785 if filename in os.listdir(cwd):
2786 if os.path.isfile(os.path.join(cwd, filename)):
2787 return open(os.path.join(cwd, filename))
2788 if cwd == root:
2789 break
2790 cwd = os.path.dirname(cwd)
2791
2792
2793def LoadCodereviewSettingsFromFile(fileobj):
2794 """Parse a codereview.settings file and updates hooks."""
[email protected]99ac1c52012-01-16 14:52:122795 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
[email protected]cc51cd02010-12-23 00:48:392796
[email protected]cc51cd02010-12-23 00:48:392797 def SetProperty(name, setting, unset_error_ok=False):
2798 fullname = 'rietveld.' + name
2799 if setting in keyvals:
2800 RunGit(['config', fullname, keyvals[setting]])
2801 else:
2802 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2803
2804 SetProperty('server', 'CODE_REVIEW_SERVER')
2805 # Only server setting is required. Other settings can be absent.
2806 # In that case, we ignore errors raised during option deletion attempt.
2807 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
[email protected]c1737d02013-05-29 14:17:282808 SetProperty('private', 'PRIVATE', unset_error_ok=True)
[email protected]cc51cd02010-12-23 00:48:392809 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2810 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
[email protected]90752582014-01-14 21:04:502811 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
[email protected]44202a22014-03-11 19:22:182812 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
[email protected]6abc6522014-12-02 07:34:492813 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2814 unset_error_ok=True)
[email protected]44202a22014-03-11 19:22:182815 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
[email protected]152cf832014-06-11 21:37:492816 SetProperty('project', 'PROJECT', unset_error_ok=True)
[email protected]566a02a2014-08-22 01:34:132817 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
[email protected]5626a922015-02-26 14:03:302818 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2819 unset_error_ok=True)
[email protected]cc51cd02010-12-23 00:48:392820
[email protected]7044efc2013-11-28 01:51:212821 if 'GERRIT_HOST' in keyvals:
[email protected]e8077812012-02-03 03:41:462822 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
[email protected]e8077812012-02-03 03:41:462823
[email protected]54b400c2016-01-14 10:08:252824 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2825 RunGit(['config', 'gerrit.squash-uploads',
2826 keyvals['GERRIT_SQUASH_UPLOADS']])
2827
[email protected]28253532016-04-14 13:46:562828 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
[email protected]00dbccd2016-04-15 07:24:432829 RunGit(['config', 'gerrit.skip-ensure-authenticated',
[email protected]28253532016-04-14 13:46:562830 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
2831
[email protected]cc51cd02010-12-23 00:48:392832 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2833 #should be of the form
2834 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2835 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2836 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2837 keyvals['ORIGIN_URL_CONFIG']])
2838
[email protected]cc51cd02010-12-23 00:48:392839
[email protected]426f69b2012-08-02 23:41:492840def urlretrieve(source, destination):
2841 """urllib is broken for SSL connections via a proxy therefore we
2842 can't use urllib.urlretrieve()."""
2843 with open(destination, 'w') as f:
2844 f.write(urllib2.urlopen(source).read())
2845
2846
[email protected]712d6102013-11-27 00:52:582847def hasSheBang(fname):
2848 """Checks fname is a #! script."""
2849 with open(fname) as f:
2850 return f.read(2).startswith('#!')
2851
2852
[email protected]917f0ff2016-04-05 00:45:302853# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2854def DownloadHooks(*args, **kwargs):
2855 pass
2856
2857
[email protected]18630d62016-03-04 12:06:022858def DownloadGerritHook(force):
2859 """Download and install Gerrit commit-msg hook.
[email protected]78c4b982012-02-14 02:20:262860
2861 Args:
2862 force: True to update hooks. False to install hooks if not present.
2863 """
2864 if not settings.GetIsGerrit():
2865 return
[email protected]712d6102013-11-27 00:52:582866 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
[email protected]78c4b982012-02-14 02:20:262867 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2868 if not os.access(dst, os.X_OK):
2869 if os.path.exists(dst):
2870 if not force:
2871 return
[email protected]78c4b982012-02-14 02:20:262872 try:
[email protected]18630d62016-03-04 12:06:022873 print(
2874 'WARNING: installing Gerrit commit-msg hook.\n'
2875 ' This behavior of git cl will soon be disabled.\n'
2876 ' See bug http://crbug.com/579176.')
[email protected]426f69b2012-08-02 23:41:492877 urlretrieve(src, dst)
[email protected]712d6102013-11-27 00:52:582878 if not hasSheBang(dst):
2879 DieWithError('Not a script: %s\n'
2880 'You need to download from\n%s\n'
2881 'into .git/hooks/commit-msg and '
2882 'chmod +x .git/hooks/commit-msg' % (dst, src))
[email protected]78c4b982012-02-14 02:20:262883 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2884 except Exception:
2885 if os.path.exists(dst):
2886 os.remove(dst)
[email protected]712d6102013-11-27 00:52:582887 DieWithError('\nFailed to download hooks.\n'
2888 'You need to download from\n%s\n'
2889 'into .git/hooks/commit-msg and '
2890 'chmod +x .git/hooks/commit-msg' % src)
[email protected]78c4b982012-02-14 02:20:262891
2892
[email protected]e7d3d162016-03-15 14:15:572893
2894def GetRietveldCodereviewSettingsInteractively():
2895 """Prompt the user for settings."""
2896 server = settings.GetDefaultServerUrl(error_ok=True)
2897 prompt = 'Rietveld server (host[:port])'
2898 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2899 newserver = ask_for_data(prompt + ':')
2900 if not server and not newserver:
2901 newserver = DEFAULT_SERVER
2902 if newserver:
2903 newserver = gclient_utils.UpgradeToHttps(newserver)
2904 if newserver != server:
2905 RunGit(['config', 'rietveld.server', newserver])
2906
2907 def SetProperty(initial, caption, name, is_url):
2908 prompt = caption
2909 if initial:
2910 prompt += ' ("x" to clear) [%s]' % initial
2911 new_val = ask_for_data(prompt + ':')
2912 if new_val == 'x':
2913 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2914 elif new_val:
2915 if is_url:
2916 new_val = gclient_utils.UpgradeToHttps(new_val)
2917 if new_val != initial:
2918 RunGit(['config', 'rietveld.' + name, new_val])
2919
2920 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2921 SetProperty(settings.GetDefaultPrivateFlag(),
2922 'Private flag (rietveld only)', 'private', False)
2923 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2924 'tree-status-url', False)
2925 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2926 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2927 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2928 'run-post-upload-hook', False)
2929
[email protected]0633fb42013-08-16 20:06:142930@subcommand.usage('[repo root containing codereview.settings]')
[email protected]cc51cd02010-12-23 00:48:392931def CMDconfig(parser, args):
[email protected]d9c1b202013-07-24 23:52:112932 """Edits configuration for this tree."""
[email protected]cc51cd02010-12-23 00:48:392933
[email protected]e7d3d162016-03-15 14:15:572934 print('WARNING: git cl config works for Rietveld only.\n'
[email protected]8930b3d2016-04-13 14:47:022935 'For Gerrit, see http://crbug.com/603116.')
2936 # TODO(tandrii): add Gerrit support as part of http://crbug.com/603116.
[email protected]87884cc2014-01-03 22:23:412937 parser.add_option('--activate-update', action='store_true',
2938 help='activate auto-updating [rietveld] section in '
2939 '.git/config')
2940 parser.add_option('--deactivate-update', action='store_true',
2941 help='deactivate auto-updating [rietveld] section in '
2942 '.git/config')
2943 options, args = parser.parse_args(args)
2944
2945 if options.deactivate_update:
2946 RunGit(['config', 'rietveld.autoupdate', 'false'])
2947 return
2948
2949 if options.activate_update:
2950 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2951 return
2952
[email protected]cc51cd02010-12-23 00:48:392953 if len(args) == 0:
[email protected]e7d3d162016-03-15 14:15:572954 GetRietveldCodereviewSettingsInteractively()
[email protected]cc51cd02010-12-23 00:48:392955 return 0
2956
2957 url = args[0]
2958 if not url.endswith('codereview.settings'):
2959 url = os.path.join(url, 'codereview.settings')
2960
2961 # Load code review settings and download hooks (if available).
2962 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
[email protected]cc51cd02010-12-23 00:48:392963 return 0
2964
2965
[email protected]6b0051e2012-04-03 15:45:082966def CMDbaseurl(parser, args):
[email protected]d9c1b202013-07-24 23:52:112967 """Gets or sets base-url for this branch."""
[email protected]6b0051e2012-04-03 15:45:082968 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2969 branch = ShortBranchName(branchref)
2970 _, args = parser.parse_args(args)
2971 if not args:
2972 print("Current base-url:")
2973 return RunGit(['config', 'branch.%s.base-url' % branch],
2974 error_ok=False).strip()
2975 else:
2976 print("Setting base-url to %s" % args[0])
2977 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2978 error_ok=False).strip()
2979
2980
[email protected]b99fbd92014-09-11 17:29:282981def color_for_status(status):
2982 """Maps a Changelist status to color, for CMDstatus and other tools."""
2983 return {
2984 'unsent': Fore.RED,
2985 'waiting': Fore.BLUE,
2986 'reply': Fore.YELLOW,
2987 'lgtm': Fore.GREEN,
2988 'commit': Fore.MAGENTA,
2989 'closed': Fore.CYAN,
2990 'error': Fore.WHITE,
2991 }.get(status, Fore.WHITE)
2992
[email protected]04ea8462016-04-25 19:51:212993
[email protected]cbd7dc32016-05-31 10:33:502994def get_cl_statuses(changes, fine_grained, max_processes=None):
2995 """Returns a blocking iterable of (cl, status) for given branches.
[email protected]ffde55c2015-03-12 00:44:172996
2997 If fine_grained is true, this will fetch CL statuses from the server.
2998 Otherwise, simply indicate if there's a matching url for the given branches.
2999
3000 If max_processes is specified, it is used as the maximum number of processes
3001 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3002 spawned.
[email protected]cf197482016-04-29 20:15:533003
3004 See GetStatus() for a list of possible statuses.
[email protected]ffde55c2015-03-12 00:44:173005 """
3006 # Silence upload.py otherwise it becomes unwieldly.
3007 upload.verbosity = 0
3008
3009 if fine_grained:
3010 # Process one branch synchronously to work through authentication, then
3011 # spawn processes to process all the other branches in parallel.
[email protected]cbd7dc32016-05-31 10:33:503012 if changes:
3013 fetch = lambda cl: (cl, cl.GetStatus())
3014 yield fetch(changes[0])
[email protected]cf197482016-04-29 20:15:533015
kmarshall3bff56b2016-06-07 01:31:473016 if not changes:
3017 # Exit early if there was only one branch to fetch.
3018 return
3019
[email protected]cbd7dc32016-05-31 10:33:503020 changes_to_fetch = changes[1:]
[email protected]ffde55c2015-03-12 00:44:173021 pool = ThreadPool(
[email protected]cbd7dc32016-05-31 10:33:503022 min(max_processes, len(changes_to_fetch))
[email protected]ffde55c2015-03-12 00:44:173023 if max_processes is not None
[email protected]cbd7dc32016-05-31 10:33:503024 else len(changes_to_fetch))
[email protected]cf197482016-04-29 20:15:533025
[email protected]cbd7dc32016-05-31 10:33:503026 fetched_cls = set()
3027 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
[email protected]cf197482016-04-29 20:15:533028 while True:
3029 try:
3030 row = it.next(timeout=5)
3031 except multiprocessing.TimeoutError:
3032 break
3033
[email protected]cbd7dc32016-05-31 10:33:503034 fetched_cls.add(row[0])
[email protected]cf197482016-04-29 20:15:533035 yield row
3036
3037 # Add any branches that failed to fetch.
[email protected]cbd7dc32016-05-31 10:33:503038 for cl in set(changes_to_fetch) - fetched_cls:
3039 yield (cl, 'error')
[email protected]cf197482016-04-29 20:15:533040
[email protected]ffde55c2015-03-12 00:44:173041 else:
3042 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
[email protected]cbd7dc32016-05-31 10:33:503043 for cl in changes:
3044 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
[email protected]b99fbd92014-09-11 17:29:283045
[email protected]2dd99862015-06-22 12:22:183046
3047def upload_branch_deps(cl, args):
3048 """Uploads CLs of local branches that are dependents of the current branch.
3049
3050 If the local branch dependency tree looks like:
3051 test1 -> test2.1 -> test3.1
3052 -> test3.2
3053 -> test2.2 -> test3.3
3054
3055 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3056 run on the dependent branches in this order:
3057 test2.1, test3.1, test3.2, test2.2, test3.3
3058
3059 Note: This function does not rebase your local dependent branches. Use it when
3060 you make a change to the parent branch that will not conflict with its
3061 dependent branches, and you would like their dependencies updated in
3062 Rietveld.
3063 """
3064 if git_common.is_dirty_git_tree('upload-branch-deps'):
3065 return 1
3066
3067 root_branch = cl.GetBranch()
3068 if root_branch is None:
3069 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3070 'Get on a branch!')
3071 if not cl.GetIssue() or not cl.GetPatchset():
3072 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3073 'patchset dependencies without an uploaded CL.')
3074
3075 branches = RunGit(['for-each-ref',
3076 '--format=%(refname:short) %(upstream:short)',
3077 'refs/heads'])
3078 if not branches:
3079 print('No local branches found.')
3080 return 0
3081
3082 # Create a dictionary of all local branches to the branches that are dependent
3083 # on it.
3084 tracked_to_dependents = collections.defaultdict(list)
3085 for b in branches.splitlines():
3086 tokens = b.split()
3087 if len(tokens) == 2:
3088 branch_name, tracked = tokens
3089 tracked_to_dependents[tracked].append(branch_name)
3090
3091 print
3092 print 'The dependent local branches of %s are:' % root_branch
3093 dependents = []
3094 def traverse_dependents_preorder(branch, padding=''):
3095 dependents_to_process = tracked_to_dependents.get(branch, [])
3096 padding += ' '
3097 for dependent in dependents_to_process:
3098 print '%s%s' % (padding, dependent)
3099 dependents.append(dependent)
3100 traverse_dependents_preorder(dependent, padding)
3101 traverse_dependents_preorder(root_branch)
3102 print
3103
3104 if not dependents:
3105 print 'There are no dependent local branches for %s' % root_branch
3106 return 0
3107
3108 print ('This command will checkout all dependent branches and run '
3109 '"git cl upload".')
3110 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3111
[email protected]962f9462016-02-03 20:00:423112 # Add a default patchset title to all upload calls in Rietveld.
[email protected]4c72b082016-03-31 22:26:353113 if not cl.IsGerrit():
[email protected]962f9462016-02-03 20:00:423114 args.extend(['-t', 'Updated patchset dependency'])
3115
[email protected]2dd99862015-06-22 12:22:183116 # Record all dependents that failed to upload.
3117 failures = {}
3118 # Go through all dependents, checkout the branch and upload.
3119 try:
3120 for dependent_branch in dependents:
3121 print
3122 print '--------------------------------------'
3123 print 'Running "git cl upload" from %s:' % dependent_branch
3124 RunGit(['checkout', '-q', dependent_branch])
3125 print
3126 try:
3127 if CMDupload(OptionParser(), args) != 0:
3128 print 'Upload failed for %s!' % dependent_branch
3129 failures[dependent_branch] = 1
3130 except: # pylint: disable=W0702
3131 failures[dependent_branch] = 1
3132 print
3133 finally:
3134 # Swap back to the original root branch.
3135 RunGit(['checkout', '-q', root_branch])
3136
3137 print
3138 print 'Upload complete for dependent branches!'
3139 for dependent_branch in dependents:
3140 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
3141 print ' %s : %s' % (dependent_branch, upload_status)
3142 print
3143
3144 return 0
3145
3146
kmarshall3bff56b2016-06-07 01:31:473147def CMDarchive(parser, args):
3148 """Archives and deletes branches associated with closed changelists."""
3149 parser.add_option(
3150 '-j', '--maxjobs', action='store', type=int,
3151 help='The maximum number of jobs to use when retrieving review status')
3152 parser.add_option(
3153 '-f', '--force', action='store_true',
3154 help='Bypasses the confirmation prompt.')
3155
3156 auth.add_auth_options(parser)
3157 options, args = parser.parse_args(args)
3158 if args:
3159 parser.error('Unsupported args: %s' % ' '.join(args))
3160 auth_config = auth.extract_auth_config_from_options(options)
3161
3162 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3163 if not branches:
3164 return 0
3165
3166 print 'Finding all branches associated with closed issues...'
3167 changes = [Changelist(branchref=b, auth_config=auth_config)
3168 for b in branches.splitlines()]
3169 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3170 statuses = get_cl_statuses(changes,
3171 fine_grained=True,
3172 max_processes=options.maxjobs)
3173 proposal = [(cl.GetBranch(),
3174 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3175 for cl, status in statuses
3176 if status == 'closed']
3177 proposal.sort()
3178
3179 if not proposal:
3180 print 'No branches with closed codereview issues found.'
3181 return 0
3182
3183 current_branch = GetCurrentBranch()
3184
3185 print '\nBranches with closed issues that will be archived:\n'
3186 print '%*s | %s' % (alignment, 'Branch name', 'Archival tag name')
3187 for next_item in proposal:
3188 print '%*s %s' % (alignment, next_item[0], next_item[1])
3189
3190 if any(branch == current_branch for branch, _ in proposal):
3191 print('You are currently on a branch \'%s\' which is associated with a '
3192 'closed codereview issue, so archive cannot proceed. Please '
3193 'checkout another branch and run this command again.' %
3194 current_branch)
3195 return 1
3196
3197 if not options.force:
3198 if ask_for_data('\nProceed with deletion (Y/N)? ').lower() != 'y':
3199 print 'Aborted.'
3200 return 1
3201
3202 for branch, tagname in proposal:
3203 RunGit(['tag', tagname, branch])
3204 RunGit(['branch', '-D', branch])
3205 print '\nJob\'s done!'
3206
3207 return 0
3208
3209
[email protected]cc51cd02010-12-23 00:48:393210def CMDstatus(parser, args):
[email protected]d9c1b202013-07-24 23:52:113211 """Show status of changelists.
3212
3213 Colors are used to tell the state of the CL unless --fast is used:
[email protected]aeab41a2013-12-10 20:01:223214 - Red not sent for review or broken
3215 - Blue waiting for review
3216 - Yellow waiting for you to reply to review
3217 - Green LGTM'ed
3218 - Magenta in the commit queue
3219 - Cyan was committed, branch can be deleted
[email protected]d9c1b202013-07-24 23:52:113220
3221 Also see 'git cl comments'.
3222 """
[email protected]cc51cd02010-12-23 00:48:393223 parser.add_option('--field',
3224 help='print only specific field (desc|id|patch|url)')
[email protected]1033efd2013-07-23 23:25:093225 parser.add_option('-f', '--fast', action='store_true',
3226 help='Do not retrieve review status')
[email protected]ffde55c2015-03-12 00:44:173227 parser.add_option(
3228 '-j', '--maxjobs', action='store', type=int,
3229 help='The maximum number of jobs to use when retrieving review status')
[email protected]cf6a5d22015-04-09 22:02:003230
3231 auth.add_auth_options(parser)
3232 options, args = parser.parse_args(args)
[email protected]39c0b222013-08-17 16:57:013233 if args:
3234 parser.error('Unsupported args: %s' % args)
[email protected]cf6a5d22015-04-09 22:02:003235 auth_config = auth.extract_auth_config_from_options(options)
[email protected]cc51cd02010-12-23 00:48:393236
[email protected]cc51cd02010-12-23 00:48:393237 if options.field:
[email protected]cf6a5d22015-04-09 22:02:003238 cl = Changelist(auth_config=auth_config)
[email protected]cc51cd02010-12-23 00:48:393239 if options.field.startswith('desc'):
3240 print cl.GetDescription()
3241 elif options.field == 'id':
3242 issueid = cl.GetIssue()
3243 if issueid:
3244 print issueid
3245 elif options.field == 'patch':
3246 patchset = cl.GetPatchset()
3247 if patchset:
3248 print patchset
3249 elif options.field == 'url':
3250 url = cl.GetIssueURL()
3251 if url:
3252 print url
[email protected]e25c75b2013-07-23 18:30:563253 return 0
3254
3255 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3256 if not branches:
3257 print('No local branch found.')
3258 return 0
3259
[email protected]cbd7dc32016-05-31 10:33:503260 changes = [
[email protected]cf6a5d22015-04-09 22:02:003261 Changelist(branchref=b, auth_config=auth_config)
[email protected]cbd7dc32016-05-31 10:33:503262 for b in branches.splitlines()]
[email protected]e25c75b2013-07-23 18:30:563263 print 'Branches associated with reviews:'
[email protected]cbd7dc32016-05-31 10:33:503264 output = get_cl_statuses(changes,
[email protected]ffde55c2015-03-12 00:44:173265 fine_grained=not options.fast,
[email protected]cbd7dc32016-05-31 10:33:503266 max_processes=options.maxjobs)
[email protected]1033efd2013-07-23 23:25:093267
[email protected]ffde55c2015-03-12 00:44:173268 branch_statuses = {}
[email protected]cbd7dc32016-05-31 10:33:503269 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3270 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3271 branch = cl.GetBranch()
[email protected]ffde55c2015-03-12 00:44:173272 while branch not in branch_statuses:
[email protected]cbd7dc32016-05-31 10:33:503273 c, status = output.next()
3274 branch_statuses[c.GetBranch()] = status
3275 status = branch_statuses.pop(branch)
3276 url = cl.GetIssueURL()
3277 if url and (not status or status == 'error'):
3278 # The issue probably doesn't exist anymore.
3279 url += ' (broken)'
3280
[email protected]a6de1f42015-06-10 04:23:173281 color = color_for_status(status)
[email protected]885f6512013-07-27 02:17:263282 reset = Fore.RESET
[email protected]596cd5c2016-04-04 21:34:393283 if not setup_color.IS_TTY:
[email protected]885f6512013-07-27 02:17:263284 color = ''
3285 reset = ''
[email protected]a6de1f42015-06-10 04:23:173286 status_str = '(%s)' % status if status else ''
3287 print ' %*s : %s%s %s%s' % (
[email protected]cbd7dc32016-05-31 10:33:503288 alignment, ShortBranchName(branch), color, url,
3289 status_str, reset)
[email protected]1033efd2013-07-23 23:25:093290
[email protected]cf6a5d22015-04-09 22:02:003291 cl = Changelist(auth_config=auth_config)
[email protected]e25c75b2013-07-23 18:30:563292 print
3293 print 'Current branch:',
[email protected]e25c75b2013-07-23 18:30:563294 print cl.GetBranch()
[email protected]ee87f582015-07-31 18:46:253295 if not cl.GetIssue():
3296 print 'No issue assigned.'
3297 return 0
[email protected]e25c75b2013-07-23 18:30:563298 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
[email protected]85616e02014-07-28 15:37:553299 if not options.fast:
3300 print 'Issue description:'
3301 print cl.GetDescription(pretty=True)
[email protected]cc51cd02010-12-23 00:48:393302 return 0
3303
3304
[email protected]39c0b222013-08-17 16:57:013305def colorize_CMDstatus_doc():
3306 """To be called once in main() to add colors to git cl status help."""
3307 colors = [i for i in dir(Fore) if i[0].isupper()]
3308
3309 def colorize_line(line):
3310 for color in colors:
3311 if color in line.upper():
3312 # Extract whitespaces first and the leading '-'.
3313 indent = len(line) - len(line.lstrip(' ')) + 1
3314 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3315 return line
3316
3317 lines = CMDstatus.__doc__.splitlines()
3318 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3319
3320
[email protected]0633fb42013-08-16 20:06:143321@subcommand.usage('[issue_number]')
[email protected]cc51cd02010-12-23 00:48:393322def CMDissue(parser, args):
[email protected]d9c1b202013-07-24 23:52:113323 """Sets or displays the current code review issue number.
[email protected]cc51cd02010-12-23 00:48:393324
3325 Pass issue number 0 to clear the current issue.
[email protected]d9c1b202013-07-24 23:52:113326 """
[email protected]406c4402015-03-03 17:22:283327 parser.add_option('-r', '--reverse', action='store_true',
3328 help='Lookup the branch(es) for the specified issues. If '
3329 'no issues are specified, all branches with mapped '
3330 'issues will be listed.')
[email protected]dde64622016-04-13 17:11:213331 _add_codereview_select_options(parser)
[email protected]406c4402015-03-03 17:22:283332 options, args = parser.parse_args(args)
[email protected]dde64622016-04-13 17:11:213333 _process_codereview_select_options(parser, options)
[email protected]cc51cd02010-12-23 00:48:393334
[email protected]406c4402015-03-03 17:22:283335 if options.reverse:
3336 branches = RunGit(['for-each-ref', 'refs/heads',
3337 '--format=%(refname:short)']).splitlines()
3338
3339 # Reverse issue lookup.
3340 issue_branch_map = {}
3341 for branch in branches:
3342 cl = Changelist(branchref=branch)
3343 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3344 if not args:
3345 args = sorted(issue_branch_map.iterkeys())
3346 for issue in args:
3347 if not issue:
3348 continue
3349 print 'Branch for issue number %s: %s' % (
3350 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3351 else:
[email protected]dde64622016-04-13 17:11:213352 cl = Changelist(codereview=options.forced_codereview)
[email protected]406c4402015-03-03 17:22:283353 if len(args) > 0:
3354 try:
3355 issue = int(args[0])
3356 except ValueError:
3357 DieWithError('Pass a number to set the issue or none to list it.\n'
[email protected]8930b3d2016-04-13 14:47:023358 'Maybe you want to run git cl status?')
[email protected]406c4402015-03-03 17:22:283359 cl.SetIssue(issue)
3360 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
[email protected]cc51cd02010-12-23 00:48:393361 return 0
3362
3363
[email protected]9977a2e2012-06-06 22:30:563364def CMDcomments(parser, args):
[email protected]e4efd512014-11-05 09:05:293365 """Shows or posts review comments for any changelist."""
3366 parser.add_option('-a', '--add-comment', dest='comment',
3367 help='comment to add to an issue')
3368 parser.add_option('-i', dest='issue',
3369 help="review issue id (defaults to current issue)")
[email protected]c85ac942015-09-15 16:34:433370 parser.add_option('-j', '--json-file',
3371 help='File to write JSON summary to')
[email protected]cf6a5d22015-04-09 22:02:003372 auth.add_auth_options(parser)
[email protected]e4efd512014-11-05 09:05:293373 options, args = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:003374 auth_config = auth.extract_auth_config_from_options(options)
[email protected]9977a2e2012-06-06 22:30:563375
[email protected]e4efd512014-11-05 09:05:293376 issue = None
3377 if options.issue:
3378 try:
3379 issue = int(options.issue)
3380 except ValueError:
3381 DieWithError('A review issue id is expected to be a number')
3382
[email protected]aa5ced12016-03-29 09:41:143383 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
[email protected]e4efd512014-11-05 09:05:293384
3385 if options.comment:
3386 cl.AddComment(options.comment)
3387 return 0
3388
3389 data = cl.GetIssueProperties()
[email protected]c85ac942015-09-15 16:34:433390 summary = []
[email protected]5cab2d32014-11-11 18:32:413391 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
[email protected]c85ac942015-09-15 16:34:433392 summary.append({
3393 'date': message['date'],
3394 'lgtm': False,
3395 'message': message['text'],
3396 'not_lgtm': False,
3397 'sender': message['sender'],
3398 })
[email protected]e4efd512014-11-05 09:05:293399 if message['disapproval']:
3400 color = Fore.RED
[email protected]c85ac942015-09-15 16:34:433401 summary[-1]['not lgtm'] = True
[email protected]e4efd512014-11-05 09:05:293402 elif message['approval']:
3403 color = Fore.GREEN
[email protected]c85ac942015-09-15 16:34:433404 summary[-1]['lgtm'] = True
[email protected]e4efd512014-11-05 09:05:293405 elif message['sender'] == data['owner_email']:
3406 color = Fore.MAGENTA
3407 else:
3408 color = Fore.BLUE
3409 print '\n%s%s %s%s' % (
3410 color, message['date'].split('.', 1)[0], message['sender'],
3411 Fore.RESET)
3412 if message['text'].strip():
3413 print '\n'.join(' ' + l for l in message['text'].splitlines())
[email protected]c85ac942015-09-15 16:34:433414 if options.json_file:
3415 with open(options.json_file, 'wb') as f:
3416 json.dump(summary, f)
[email protected]9977a2e2012-06-06 22:30:563417 return 0
3418
3419
[email protected]2b55fe32016-04-26 20:28:543420@subcommand.usage('[codereview url or issue id]')
[email protected]eec76592013-05-20 16:27:573421def CMDdescription(parser, args):
[email protected]d9c1b202013-07-24 23:52:113422 """Brings up the editor for the current CL's description."""
[email protected]34fb6b12015-07-13 20:03:263423 parser.add_option('-d', '--display', action='store_true',
3424 help='Display the description instead of opening an editor')
[email protected]d6648e22016-04-29 19:22:163425 parser.add_option('-n', '--new-description',
3426 help='New description to set for this issue (- for stdin)')
[email protected]2b55fe32016-04-26 20:28:543427
3428 _add_codereview_select_options(parser)
[email protected]cf6a5d22015-04-09 22:02:003429 auth.add_auth_options(parser)
[email protected]2b55fe32016-04-26 20:28:543430 options, args = parser.parse_args(args)
3431 _process_codereview_select_options(parser, options)
3432
3433 target_issue = None
3434 if len(args) > 0:
3435 issue_arg = ParseIssueNumberArgument(args[0])
3436 if not issue_arg.valid:
3437 parser.print_help()
3438 return 1
3439 target_issue = issue_arg.issue
3440
[email protected]cf6a5d22015-04-09 22:02:003441 auth_config = auth.extract_auth_config_from_options(options)
[email protected]2b55fe32016-04-26 20:28:543442
3443 cl = Changelist(
3444 auth_config=auth_config, issue=target_issue,
3445 codereview=options.forced_codereview)
3446
[email protected]eec76592013-05-20 16:27:573447 if not cl.GetIssue():
3448 DieWithError('This branch has no associated changelist.')
3449 description = ChangeDescription(cl.GetDescription())
[email protected]d6648e22016-04-29 19:22:163450
[email protected]34fb6b12015-07-13 20:03:263451 if options.display:
[email protected]8c3b4422016-04-27 13:11:183452 print description.description
[email protected]34fb6b12015-07-13 20:03:263453 return 0
[email protected]d6648e22016-04-29 19:22:163454
3455 if options.new_description:
3456 text = options.new_description
3457 if text == '-':
3458 text = '\n'.join(l.rstrip() for l in sys.stdin)
3459
3460 description.set_description(text)
3461 else:
3462 description.prompt()
3463
[email protected]063e4e52015-04-03 06:51:443464 if cl.GetDescription() != description.description:
3465 cl.UpdateDescription(description.description)
[email protected]eec76592013-05-20 16:27:573466 return 0
3467
3468
[email protected]cc51cd02010-12-23 00:48:393469def CreateDescriptionFromLog(args):
3470 """Pulls out the commit log to use as a base for the CL description."""
3471 log_args = []
3472 if len(args) == 1 and not args[0].endswith('.'):
3473 log_args = [args[0] + '..']
3474 elif len(args) == 1 and args[0].endswith('...'):
3475 log_args = [args[0][:-1]]
3476 elif len(args) == 2:
3477 log_args = [args[0] + '..' + args[1]]
3478 else:
3479 log_args = args[:] # Hope for the best!
[email protected]373af802012-05-25 21:07:333480 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
[email protected]cc51cd02010-12-23 00:48:393481
3482
[email protected]44202a22014-03-11 19:22:183483def CMDlint(parser, args):
3484 """Runs cpplint on the current changelist."""
[email protected]f204d4b2014-03-13 07:40:553485 parser.add_option('--filter', action='append', metavar='-x,+y',
3486 help='Comma-separated list of cpplint\'s category-filters')
[email protected]cf6a5d22015-04-09 22:02:003487 auth.add_auth_options(parser)
3488 options, args = parser.parse_args(args)
3489 auth_config = auth.extract_auth_config_from_options(options)
[email protected]44202a22014-03-11 19:22:183490
3491 # Access to a protected member _XX of a client class
3492 # pylint: disable=W0212
3493 try:
3494 import cpplint
3495 import cpplint_chromium
3496 except ImportError:
3497 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3498 return 1
3499
3500 # Change the current working directory before calling lint so that it
3501 # shows the correct base.
3502 previous_cwd = os.getcwd()
3503 os.chdir(settings.GetRoot())
3504 try:
[email protected]cf6a5d22015-04-09 22:02:003505 cl = Changelist(auth_config=auth_config)
[email protected]44202a22014-03-11 19:22:183506 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3507 files = [f.LocalPath() for f in change.AffectedFiles()]
[email protected]5839eb52014-05-30 16:20:513508 if not files:
3509 print "Cannot lint an empty CL"
3510 return 1
[email protected]44202a22014-03-11 19:22:183511
3512 # Process cpplints arguments if any.
[email protected]f204d4b2014-03-13 07:40:553513 command = args + files
3514 if options.filter:
3515 command = ['--filter=' + ','.join(options.filter)] + command
3516 filenames = cpplint.ParseArguments(command)
[email protected]44202a22014-03-11 19:22:183517
3518 white_regex = re.compile(settings.GetLintRegex())
3519 black_regex = re.compile(settings.GetLintIgnoreRegex())
3520 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3521 for filename in filenames:
3522 if white_regex.match(filename):
3523 if black_regex.match(filename):
3524 print "Ignoring file %s" % filename
3525 else:
3526 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3527 extra_check_functions)
3528 else:
3529 print "Skipping file %s" % filename
3530 finally:
3531 os.chdir(previous_cwd)
3532 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3533 if cpplint._cpplint_state.error_count != 0:
3534 return 1
3535 return 0
3536
3537
[email protected]cc51cd02010-12-23 00:48:393538def CMDpresubmit(parser, args):
[email protected]d9c1b202013-07-24 23:52:113539 """Runs presubmit tests on the current changelist."""
[email protected]375a9022013-01-07 01:12:053540 parser.add_option('-u', '--upload', action='store_true',
[email protected]cc51cd02010-12-23 00:48:393541 help='Run upload hook instead of the push/dcommit hook')
[email protected]375a9022013-01-07 01:12:053542 parser.add_option('-f', '--force', action='store_true',
[email protected]495ad152012-09-04 23:07:423543 help='Run checks even if tree is dirty')
[email protected]cf6a5d22015-04-09 22:02:003544 auth.add_auth_options(parser)
3545 options, args = parser.parse_args(args)
3546 auth_config = auth.extract_auth_config_from_options(options)
[email protected]cc51cd02010-12-23 00:48:393547
[email protected]71437c02015-04-09 19:29:403548 if not options.force and git_common.is_dirty_git_tree('presubmit'):
[email protected]259e4682012-10-25 07:36:333549 print 'use --force to check even if tree is dirty.'
[email protected]cc51cd02010-12-23 00:48:393550 return 1
3551
[email protected]cf6a5d22015-04-09 22:02:003552 cl = Changelist(auth_config=auth_config)
[email protected]cc51cd02010-12-23 00:48:393553 if args:
3554 base_branch = args[0]
3555 else:
[email protected]0f58fa82012-11-05 01:45:203556 # Default to diffing against the common ancestor of the upstream branch.
[email protected]8b0553c2014-02-11 00:33:373557 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]cc51cd02010-12-23 00:48:393558
[email protected]051ad0e2013-03-04 21:57:343559 cl.RunHook(
3560 committing=not options.upload,
3561 may_prompt=False,
3562 verbose=options.verbose,
3563 change=cl.GetChange(base_branch, None))
[email protected]0a2bb372011-03-25 01:16:223564 return 0
[email protected]cc51cd02010-12-23 00:48:393565
3566
[email protected]65874e12016-03-04 12:03:023567def GenerateGerritChangeId(message):
3568 """Returns Ixxxxxx...xxx change id.
3569
3570 Works the same way as
3571 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3572 but can be called on demand on all platforms.
3573
3574 The basic idea is to generate git hash of a state of the tree, original commit
3575 message, author/committer info and timestamps.
3576 """
3577 lines = []
3578 tree_hash = RunGitSilent(['write-tree'])
3579 lines.append('tree %s' % tree_hash.strip())
3580 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3581 if code == 0:
3582 lines.append('parent %s' % parent.strip())
3583 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3584 lines.append('author %s' % author.strip())
3585 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3586 lines.append('committer %s' % committer.strip())
3587 lines.append('')
3588 # Note: Gerrit's commit-hook actually cleans message of some lines and
3589 # whitespace. This code is not doing this, but it clearly won't decrease
3590 # entropy.
3591 lines.append(message)
3592 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3593 stdin='\n'.join(lines))
3594 return 'I%s' % change_hash.strip()
3595
3596
[email protected]455dc922015-01-26 20:15:503597def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3598 """Computes the remote branch ref to use for the CL.
3599
3600 Args:
3601 remote (str): The git remote for the CL.
3602 remote_branch (str): The git remote branch for the CL.
3603 target_branch (str): The target branch specified by the user.
3604 pending_prefix (str): The pending prefix from the settings.
3605 """
3606 if not (remote and remote_branch):
3607 return None
[email protected]27386dd2015-02-16 10:45:393608
[email protected]455dc922015-01-26 20:15:503609 if target_branch:
3610 # Cannonicalize branch references to the equivalent local full symbolic
3611 # refs, which are then translated into the remote full symbolic refs
3612 # below.
3613 if '/' not in target_branch:
3614 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3615 else:
3616 prefix_replacements = (
3617 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3618 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3619 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3620 )
3621 match = None
3622 for regex, replacement in prefix_replacements:
3623 match = re.search(regex, target_branch)
3624 if match:
3625 remote_branch = target_branch.replace(match.group(0), replacement)
3626 break
3627 if not match:
3628 # This is a branch path but not one we recognize; use as-is.
3629 remote_branch = target_branch
[email protected]c68112d2015-03-03 12:48:063630 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3631 # Handle the refs that need to land in different refs.
3632 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
[email protected]27386dd2015-02-16 10:45:393633
[email protected]455dc922015-01-26 20:15:503634 # Create the true path to the remote branch.
3635 # Does the following translation:
3636 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3637 # * refs/remotes/origin/master -> refs/heads/master
3638 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3639 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3640 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3641 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3642 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3643 'refs/heads/')
3644 elif remote_branch.startswith('refs/remotes/branch-heads'):
3645 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3646 # If a pending prefix exists then replace refs/ with it.
3647 if pending_prefix:
3648 remote_branch = remote_branch.replace('refs/', pending_prefix)
3649 return remote_branch
3650
3651
[email protected]eb52a5c2013-04-10 23:17:093652def cleanup_list(l):
3653 """Fixes a list so that comma separated items are put as individual items.
3654
3655 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3656 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3657 """
3658 items = sum((i.split(',') for i in l), [])
3659 stripped_items = (i.strip() for i in items)
3660 return sorted(filter(None, stripped_items))
3661
3662
[email protected]0633fb42013-08-16 20:06:143663@subcommand.usage('[args to "git diff"]')
[email protected]e8077812012-02-03 03:41:463664def CMDupload(parser, args):
[email protected]78948ed2015-07-08 23:09:573665 """Uploads the current changelist to codereview.
3666
3667 Can skip dependency patchset uploads for a branch by running:
3668 git config branch.branch_name.skip-deps-uploads True
3669 To unset run:
3670 git config --unset branch.branch_name.skip-deps-uploads
3671 Can also set the above globally by using the --global flag.
3672 """
[email protected]e8077812012-02-03 03:41:463673 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3674 help='bypass upload presubmit hook')
[email protected]b65c43c2013-06-10 22:04:493675 parser.add_option('--bypass-watchlists', action='store_true',
3676 dest='bypass_watchlists',
3677 help='bypass watchlists auto CC-ing reviewers')
[email protected]e8077812012-02-03 03:41:463678 parser.add_option('-f', action='store_true', dest='force',
3679 help="force yes to questions (don't prompt)")
[email protected]420d3b82012-05-14 18:41:383680 parser.add_option('-m', dest='message', help='message for patchset')
[email protected]962f9462016-02-03 20:00:423681 parser.add_option('-t', dest='title',
3682 help='title for patchset (Rietveld only)')
[email protected]e8077812012-02-03 03:41:463683 parser.add_option('-r', '--reviewers',
[email protected]eb52a5c2013-04-10 23:17:093684 action='append', default=[],
[email protected]e8077812012-02-03 03:41:463685 help='reviewer email addresses')
3686 parser.add_option('--cc',
[email protected]eb52a5c2013-04-10 23:17:093687 action='append', default=[],
[email protected]e8077812012-02-03 03:41:463688 help='cc email addresses')
[email protected]36f47302013-04-05 01:08:313689 parser.add_option('-s', '--send-mail', action='store_true',
[email protected]e8077812012-02-03 03:41:463690 help='send email to reviewer immediately')
[email protected]b9f27512014-08-08 15:52:333691 parser.add_option('--emulate_svn_auto_props',
3692 '--emulate-svn-auto-props',
3693 action="store_true",
[email protected]e8077812012-02-03 03:41:463694 dest="emulate_svn_auto_props",
3695 help="Emulate Subversion's auto properties feature.")
[email protected]e8077812012-02-03 03:41:463696 parser.add_option('-c', '--use-commit-queue', action='store_true',
3697 help='tell the commit queue to commit this patchset')
[email protected]c1737d02013-05-29 14:17:283698 parser.add_option('--private', action='store_true',
3699 help='set the review private (rietveld only)')
[email protected]8ef7ab22012-11-28 04:24:523700 parser.add_option('--target_branch',
[email protected]b9f27512014-08-08 15:52:333701 '--target-branch',
[email protected]455dc922015-01-26 20:15:503702 metavar='TARGET',
3703 help='Apply CL to remote ref TARGET. ' +
3704 'Default: remote branch head, or master')
[email protected]27386dd2015-02-16 10:45:393705 parser.add_option('--squash', action='store_true',
3706 help='Squash multiple commits into one (Gerrit only)')
[email protected]54b400c2016-01-14 10:08:253707 parser.add_option('--no-squash', action='store_true',
3708 help='Don\'t squash multiple commits into one ' +
3709 '(Gerrit only)')
[email protected]91141372014-01-09 23:27:203710 parser.add_option('--email', default=None,
3711 help='email address to use to connect to Rietveld')
[email protected]336f9122014-09-04 02:16:553712 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3713 help='add a set of OWNERS to TBR')
[email protected]d50452a2015-11-23 16:38:153714 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3715 action='store_true',
[email protected]ef966222015-04-07 11:15:013716 help='Send the patchset to do a CQ dry run right after '
3717 'upload.')
[email protected]2dd99862015-06-22 12:22:183718 parser.add_option('--dependencies', action='store_true',
3719 help='Uploads CLs of all the local branches that depend on '
3720 'the current branch')
[email protected]91141372014-01-09 23:27:203721
[email protected]2dd99862015-06-22 12:22:183722 orig_args = args
[email protected]53937ba2012-10-02 18:20:433723 add_git_similarity(parser)
[email protected]cf6a5d22015-04-09 22:02:003724 auth.add_auth_options(parser)
[email protected]dde64622016-04-13 17:11:213725 _add_codereview_select_options(parser)
[email protected]e8077812012-02-03 03:41:463726 (options, args) = parser.parse_args(args)
[email protected]dde64622016-04-13 17:11:213727 _process_codereview_select_options(parser, options)
[email protected]cf6a5d22015-04-09 22:02:003728 auth_config = auth.extract_auth_config_from_options(options)
[email protected]e8077812012-02-03 03:41:463729
[email protected]71437c02015-04-09 19:29:403730 if git_common.is_dirty_git_tree('upload'):
[email protected]e8077812012-02-03 03:41:463731 return 1
3732
[email protected]eb52a5c2013-04-10 23:17:093733 options.reviewers = cleanup_list(options.reviewers)
3734 options.cc = cleanup_list(options.cc)
3735
[email protected]512d79c2016-03-31 12:55:283736 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3737 settings.GetIsGerrit()
3738
[email protected]dde64622016-04-13 17:11:213739 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
[email protected]9e6c3a52016-04-12 14:13:083740 return cl.CMDUpload(options, args, orig_args)
[email protected]e8077812012-02-03 03:41:463741
3742
[email protected]9bb85e22012-06-13 20:28:233743def IsSubmoduleMergeCommit(ref):
3744 # When submodules are added to the repo, we expect there to be a single
3745 # non-git-svn merge commit at remote HEAD with a signature comment.
3746 pattern = '^SVN changes up to revision [0-9]*$'
[email protected]e84b7542012-06-15 21:26:583747 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
[email protected]9bb85e22012-06-13 20:28:233748 return RunGit(cmd) != ''
3749
3750
[email protected]cc51cd02010-12-23 00:48:393751def SendUpstream(parser, args, cmd):
[email protected]cee6dc42014-05-07 17:04:033752 """Common code for CMDland and CmdDCommit
[email protected]cc51cd02010-12-23 00:48:393753
[email protected]d68b62b2016-03-31 16:09:293754 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3755 upstream and closes the issue automatically and atomically.
3756
3757 Otherwise (in case of Rietveld):
3758 Squashes branch into a single commit.
3759 Updates changelog with metadata (e.g. pointer to review).
3760 Pushes/dcommits the code upstream.
3761 Updates review and closes.
[email protected]cc51cd02010-12-23 00:48:393762 """
3763 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3764 help='bypass upload presubmit hook')
3765 parser.add_option('-m', dest='message',
3766 help="override review description")
3767 parser.add_option('-f', action='store_true', dest='force',
3768 help="force yes to questions (don't prompt)")
3769 parser.add_option('-c', dest='contributor',
3770 help="external contributor for patch (appended to " +
3771 "description and used as author for git). Should be " +
3772 "formatted as 'First Last <[email protected]>'")
[email protected]53937ba2012-10-02 18:20:433773 add_git_similarity(parser)
[email protected]cf6a5d22015-04-09 22:02:003774 auth.add_auth_options(parser)
[email protected]cc51cd02010-12-23 00:48:393775 (options, args) = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:003776 auth_config = auth.extract_auth_config_from_options(options)
3777
3778 cl = Changelist(auth_config=auth_config)
[email protected]cc51cd02010-12-23 00:48:393779
[email protected]d68b62b2016-03-31 16:09:293780 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3781 if cl.IsGerrit():
3782 if options.message:
3783 # This could be implemented, but it requires sending a new patch to
3784 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3785 # Besides, Gerrit has the ability to change the commit message on submit
3786 # automatically, thus there is no need to support this option (so far?).
3787 parser.error('-m MESSAGE option is not supported for Gerrit.')
3788 if options.contributor:
3789 parser.error(
3790 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3791 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3792 'the contributor\'s "name <email>". If you can\'t upload such a '
3793 'commit for review, contact your repository admin and request'
3794 '"Forge-Author" permission.')
3795 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3796 options.verbose)
3797
[email protected]5724c962014-04-11 09:32:563798 current = cl.GetBranch()
3799 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3800 if not settings.GetIsGitSvn() and remote == '.':
3801 print
3802 print 'Attempting to push branch %r into another local branch!' % current
3803 print
3804 print 'Either reparent this branch on top of origin/master:'
3805 print ' git reparent-branch --root'
3806 print
3807 print 'OR run `git rebase-update` if you think the parent branch is already'
3808 print 'committed.'
3809 print
3810 print ' Current parent: %r' % upstream_branch
3811 return 1
3812
[email protected]566a02a2014-08-22 01:34:133813 if not args or cmd == 'land':
[email protected]cc51cd02010-12-23 00:48:393814 # Default to merging against our best guess of the upstream branch.
3815 args = [cl.GetUpstreamBranch()]
3816
[email protected]13f623c2011-07-22 16:02:233817 if options.contributor:
3818 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3819 print "Please provide contibutor as 'First Last <[email protected]>'"
3820 return 1
3821
[email protected]cc51cd02010-12-23 00:48:393822 base_branch = args[0]
[email protected]9bb85e22012-06-13 20:28:233823 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
[email protected]cc51cd02010-12-23 00:48:393824
[email protected]71437c02015-04-09 19:29:403825 if git_common.is_dirty_git_tree(cmd):
[email protected]cc51cd02010-12-23 00:48:393826 return 1
3827
3828 # This rev-list syntax means "show all commits not in my branch that
3829 # are in base_branch".
3830 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3831 base_branch]).splitlines()
3832 if upstream_commits:
3833 print ('Base branch "%s" has %d commits '
3834 'not in this branch.' % (base_branch, len(upstream_commits)))
3835 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3836 return 1
3837
[email protected]9bb85e22012-06-13 20:28:233838 # This is the revision `svn dcommit` will commit on top of.
[email protected]566a02a2014-08-22 01:34:133839 svn_head = None
3840 if cmd == 'dcommit' or base_has_submodules:
3841 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3842 '--pretty=format:%H'])
[email protected]9bb85e22012-06-13 20:28:233843
[email protected]cc51cd02010-12-23 00:48:393844 if cmd == 'dcommit':
[email protected]9bb85e22012-06-13 20:28:233845 # If the base_head is a submodule merge commit, the first parent of the
3846 # base_head should be a git-svn commit, which is what we're interested in.
3847 base_svn_head = base_branch
3848 if base_has_submodules:
3849 base_svn_head += '^1'
3850
3851 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
[email protected]cc51cd02010-12-23 00:48:393852 if extra_commits:
3853 print ('This branch has %d additional commits not upstreamed yet.'
3854 % len(extra_commits.splitlines()))
3855 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3856 'before attempting to %s.' % (base_branch, cmd))
3857 return 1
3858
[email protected]e6896b52014-08-29 01:38:033859 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
[email protected]b0a63912012-01-17 18:10:163860 if not options.bypass_hooks:
[email protected]13f623c2011-07-22 16:02:233861 author = None
3862 if options.contributor:
3863 author = re.search(r'\<(.*)\>', options.contributor).group(1)
[email protected]b0a63912012-01-17 18:10:163864 hook_results = cl.RunHook(
3865 committing=True,
[email protected]b0a63912012-01-17 18:10:163866 may_prompt=not options.force,
3867 verbose=options.verbose,
[email protected]e6896b52014-08-29 01:38:033868 change=cl.GetChange(merge_base, author))
[email protected]b0a63912012-01-17 18:10:163869 if not hook_results.should_continue():
3870 return 1
[email protected]cc51cd02010-12-23 00:48:393871
[email protected]566a02a2014-08-22 01:34:133872 # Check the tree status if the tree status URL is set.
3873 status = GetTreeStatus()
3874 if 'closed' == status:
3875 print('The tree is closed. Please wait for it to reopen. Use '
3876 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3877 return 1
3878 elif 'unknown' == status:
3879 print('Unable to determine tree status. Please verify manually and '
3880 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3881 return 1
[email protected]cc51cd02010-12-23 00:48:393882
[email protected]78936cb2013-04-11 00:17:523883 change_desc = ChangeDescription(options.message)
3884 if not change_desc.description and cl.GetIssue():
3885 change_desc = ChangeDescription(cl.GetDescription())
[email protected]cc51cd02010-12-23 00:48:393886
[email protected]78936cb2013-04-11 00:17:523887 if not change_desc.description:
[email protected]1a173982012-08-29 20:43:053888 if not cl.GetIssue() and options.bypass_hooks:
[email protected]e6896b52014-08-29 01:38:033889 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
[email protected]1a173982012-08-29 20:43:053890 else:
3891 print 'No description set.'
3892 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3893 return 1
[email protected]cc51cd02010-12-23 00:48:393894
[email protected]78936cb2013-04-11 00:17:523895 # Keep a separate copy for the commit message, because the commit message
3896 # contains the link to the Rietveld issue, while the Rietveld message contains
3897 # the commit viewvc url.
[email protected]e52678e2013-04-26 18:34:443898 # Keep a separate copy for the commit message.
3899 if cl.GetIssue():
[email protected]cf087782013-07-23 13:08:483900 change_desc.update_reviewers(cl.GetApprovingReviewers())
[email protected]e52678e2013-04-26 18:34:443901
[email protected]78936cb2013-04-11 00:17:523902 commit_desc = ChangeDescription(change_desc.description)
[email protected]cc73ad62011-07-06 17:39:263903 if cl.GetIssue():
[email protected]4c61dcc2015-06-08 22:31:293904 # Xcode won't linkify this URL unless there is a non-whitespace character
[email protected]4b39c5f2015-07-07 10:33:123905 # after it. Add a period on a new line to circumvent this. Also add a space
3906 # before the period to make sure that Gitiles continues to correctly resolve
3907 # the URL.
3908 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
[email protected]cc51cd02010-12-23 00:48:393909 if options.contributor:
[email protected]78936cb2013-04-11 00:17:523910 commit_desc.append_footer('Patch from %s.' % options.contributor)
3911
[email protected]eec3ea32013-08-15 20:31:393912 print('Description:')
3913 print(commit_desc.description)
[email protected]cc51cd02010-12-23 00:48:393914
[email protected]e6896b52014-08-29 01:38:033915 branches = [merge_base, cl.GetBranchRef()]
[email protected]cc51cd02010-12-23 00:48:393916 if not options.force:
[email protected]79540052012-10-19 23:15:263917 print_stats(options.similarity, options.find_copies, branches)
[email protected]cc51cd02010-12-23 00:48:393918
[email protected]9bb85e22012-06-13 20:28:233919 # We want to squash all this branch's commits into one commit with the proper
3920 # description. We do this by doing a "reset --soft" to the base branch (which
3921 # keeps the working copy the same), then dcommitting that. If origin/master
3922 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3923 # commit onto a branch based on the git-svn head.
[email protected]cc51cd02010-12-23 00:48:393924 MERGE_BRANCH = 'git-cl-commit'
[email protected]9bb85e22012-06-13 20:28:233925 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3926 # Delete the branches if they exist.
3927 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3928 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3929 result = RunGitWithCode(showref_cmd)
3930 if result[0] == 0:
3931 RunGit(['branch', '-D', branch])
[email protected]cc51cd02010-12-23 00:48:393932
3933 # We might be in a directory that's present in this branch but not in the
3934 # trunk. Move up to the top of the tree so that git commands that expect a
3935 # valid CWD won't fail after we check out the merge branch.
[email protected]8b0553c2014-02-11 00:33:373936 rel_base_path = settings.GetRelativeRoot()
[email protected]cc51cd02010-12-23 00:48:393937 if rel_base_path:
3938 os.chdir(rel_base_path)
3939
3940 # Stuff our change into the merge branch.
3941 # We wrap in a try...finally block so if anything goes wrong,
3942 # we clean up the branches.
[email protected]0ba7f962011-01-11 22:13:583943 retcode = -1
[email protected]e6896b52014-08-29 01:38:033944 pushed_to_pending = False
[email protected]566a02a2014-08-22 01:34:133945 pending_ref = None
[email protected]e6896b52014-08-29 01:38:033946 revision = None
[email protected]cc51cd02010-12-23 00:48:393947 try:
[email protected]b4a75c42011-03-08 08:35:383948 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
[email protected]e6896b52014-08-29 01:38:033949 RunGit(['reset', '--soft', merge_base])
[email protected]cc51cd02010-12-23 00:48:393950 if options.contributor:
[email protected]78936cb2013-04-11 00:17:523951 RunGit(
3952 [
3953 'commit', '--author', options.contributor,
3954 '-m', commit_desc.description,
3955 ])
[email protected]cc51cd02010-12-23 00:48:393956 else:
[email protected]78936cb2013-04-11 00:17:523957 RunGit(['commit', '-m', commit_desc.description])
[email protected]9bb85e22012-06-13 20:28:233958 if base_has_submodules:
3959 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3960 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3961 RunGit(['checkout', CHERRY_PICK_BRANCH])
3962 RunGit(['cherry-pick', cherry_pick_commit])
[email protected]566a02a2014-08-22 01:34:133963 if cmd == 'land':
[email protected]0f58fa82012-11-05 01:45:203964 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
[email protected]151ebcf2016-03-09 01:08:253965 mirror = settings.GetGitMirror(remote)
3966 pushurl = mirror.url if mirror else remote
[email protected]566a02a2014-08-22 01:34:133967 pending_prefix = settings.GetPendingRefPrefix()
3968 if not pending_prefix or branch.startswith(pending_prefix):
3969 # If not using refs/pending/heads/* at all, or target ref is already set
3970 # to pending, then push to the target ref directly.
3971 retcode, output = RunGitWithCode(
[email protected]151ebcf2016-03-09 01:08:253972 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
[email protected]e6896b52014-08-29 01:38:033973 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
[email protected]566a02a2014-08-22 01:34:133974 else:
3975 # Cherry-pick the change on top of pending ref and then push it.
3976 assert branch.startswith('refs/'), branch
3977 assert pending_prefix[-1] == '/', pending_prefix
3978 pending_ref = pending_prefix + branch[len('refs/'):]
[email protected]151ebcf2016-03-09 01:08:253979 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
[email protected]e6896b52014-08-29 01:38:033980 pushed_to_pending = (retcode == 0)
[email protected]34504a12014-08-29 23:51:373981 if retcode == 0:
3982 revision = RunGit(['rev-parse', 'HEAD']).strip()
[email protected]cc51cd02010-12-23 00:48:393983 else:
3984 # dcommit the merge branch.
[email protected]a1950c42014-12-05 22:15:563985 cmd_args = [
[email protected]6abc6522014-12-02 07:34:493986 'svn', 'dcommit',
3987 '-C%s' % options.similarity,
3988 '--no-rebase', '--rmdir',
3989 ]
3990 if settings.GetForceHttpsCommitUrl():
3991 # Allow forcing https commit URLs for some projects that don't allow
3992 # committing to http URLs (like Google Code).
3993 remote_url = cl.GetGitSvnRemoteUrl()
3994 if urlparse.urlparse(remote_url).scheme == 'http':
3995 remote_url = remote_url.replace('http://', 'https://')
[email protected]a1950c42014-12-05 22:15:563996 cmd_args.append('--commit-url=%s' % remote_url)
3997 _, output = RunGitWithCode(cmd_args)
[email protected]34504a12014-08-29 23:51:373998 if 'Committed r' in output:
3999 revision = re.match(
4000 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4001 logging.debug(output)
[email protected]cc51cd02010-12-23 00:48:394002 finally:
4003 # And then swap back to the original branch and clean up.
4004 RunGit(['checkout', '-q', cl.GetBranch()])
4005 RunGit(['branch', '-D', MERGE_BRANCH])
[email protected]9bb85e22012-06-13 20:28:234006 if base_has_submodules:
4007 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
[email protected]cc51cd02010-12-23 00:48:394008
[email protected]34504a12014-08-29 23:51:374009 if not revision:
[email protected]6c217b12014-08-29 22:10:594010 print 'Failed to push. If this persists, please file a bug.'
[email protected]34504a12014-08-29 23:51:374011 return 1
[email protected]6c217b12014-08-29 22:10:594012
[email protected]bbe9cc52014-09-05 18:25:514013 killed = False
[email protected]6c217b12014-08-29 22:10:594014 if pushed_to_pending:
[email protected]e6896b52014-08-29 01:38:034015 try:
4016 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4017 # We set pushed_to_pending to False, since it made it all the way to the
4018 # real ref.
4019 pushed_to_pending = False
4020 except KeyboardInterrupt:
[email protected]bbe9cc52014-09-05 18:25:514021 killed = True
[email protected]e6896b52014-08-29 01:38:034022
[email protected]cc51cd02010-12-23 00:48:394023 if cl.GetIssue():
[email protected]e6896b52014-08-29 01:38:034024 to_pending = ' to pending queue' if pushed_to_pending else ''
[email protected]cc51cd02010-12-23 00:48:394025 viewvc_url = settings.GetViewVCUrl()
[email protected]e6896b52014-08-29 01:38:034026 if not to_pending:
4027 if viewvc_url and revision:
4028 change_desc.append_footer(
4029 'Committed: %s%s' % (viewvc_url, revision))
4030 elif revision:
4031 change_desc.append_footer('Committed: %s' % (revision,))
[email protected]cc51cd02010-12-23 00:48:394032 print ('Closing issue '
4033 '(you may be prompted for your codereview password)...')
[email protected]78936cb2013-04-11 00:17:524034 cl.UpdateDescription(change_desc.description)
[email protected]cc51cd02010-12-23 00:48:394035 cl.CloseIssue()
[email protected]1033efd2013-07-23 23:25:094036 props = cl.GetIssueProperties()
[email protected]34b5d822013-02-18 01:39:244037 patch_num = len(props['patchsets'])
[email protected]52d224a2014-08-27 14:44:414038 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
[email protected]782570c2014-09-26 21:48:024039 patch_num, props['patchsets'][-1], to_pending, revision)
[email protected]3ec0d542014-01-14 20:00:034040 if options.bypass_hooks:
4041 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4042 else:
4043 comment += ' (presubmit successful).'
[email protected]b85a3162013-01-26 01:11:134044 cl.RpcServer().add_comment(cl.GetIssue(), comment)
[email protected]1033efd2013-07-23 23:25:094045 cl.SetIssue(None)
[email protected]0ba7f962011-01-11 22:13:584046
[email protected]6c217b12014-08-29 22:10:594047 if pushed_to_pending:
[email protected]566a02a2014-08-22 01:34:134048 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4049 print 'The commit is in the pending queue (%s).' % pending_ref
4050 print (
[email protected]5f32a962014-09-05 21:33:234051 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
[email protected]566a02a2014-08-22 01:34:134052 'footer.' % branch)
4053
[email protected]6c217b12014-08-29 22:10:594054 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4055 if os.path.isfile(hook):
4056 RunCommand([hook, merge_base], error_ok=True)
[email protected]0ba7f962011-01-11 22:13:584057
[email protected]bbe9cc52014-09-05 18:25:514058 return 1 if killed else 0
[email protected]cc51cd02010-12-23 00:48:394059
4060
[email protected]e6896b52014-08-29 01:38:034061def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
4062 print
4063 print 'Waiting for commit to be landed on %s...' % real_ref
4064 print '(If you are impatient, you may Ctrl-C once without harm)'
4065 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4066 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
[email protected]151ebcf2016-03-09 01:08:254067 mirror = settings.GetGitMirror(remote)
[email protected]e6896b52014-08-29 01:38:034068
4069 loop = 0
4070 while True:
4071 sys.stdout.write('fetching (%d)... \r' % loop)
4072 sys.stdout.flush()
4073 loop += 1
4074
[email protected]151ebcf2016-03-09 01:08:254075 if mirror:
4076 mirror.populate()
[email protected]e6896b52014-08-29 01:38:034077 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4078 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4079 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4080 for commit in commits.splitlines():
4081 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
4082 print 'Found commit on %s' % real_ref
4083 return commit
4084
4085 current_rev = to_rev
4086
4087
[email protected]566a02a2014-08-22 01:34:134088def PushToGitPending(remote, pending_ref, upstream_ref):
4089 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4090
4091 Returns:
4092 (retcode of last operation, output log of last operation).
4093 """
4094 assert pending_ref.startswith('refs/'), pending_ref
4095 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4096 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4097 code = 0
4098 out = ''
[email protected]749fbd92014-08-26 21:57:534099 max_attempts = 3
4100 attempts_left = max_attempts
4101 while attempts_left:
4102 if attempts_left != max_attempts:
4103 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
4104 attempts_left -= 1
[email protected]566a02a2014-08-22 01:34:134105
4106 # Fetch. Retry fetch errors.
[email protected]749fbd92014-08-26 21:57:534107 print 'Fetching pending ref %s...' % pending_ref
[email protected]566a02a2014-08-22 01:34:134108 code, out = RunGitWithCode(
[email protected]749fbd92014-08-26 21:57:534109 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
[email protected]566a02a2014-08-22 01:34:134110 if code:
[email protected]749fbd92014-08-26 21:57:534111 print 'Fetch failed with exit code %d.' % code
4112 if out.strip():
4113 print out.strip()
[email protected]566a02a2014-08-22 01:34:134114 continue
4115
4116 # Try to cherry pick. Abort on merge conflicts.
[email protected]749fbd92014-08-26 21:57:534117 print 'Cherry-picking commit on top of pending ref...'
[email protected]566a02a2014-08-22 01:34:134118 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
[email protected]749fbd92014-08-26 21:57:534119 code, out = RunGitWithCode(['cherry-pick', cherry])
[email protected]566a02a2014-08-22 01:34:134120 if code:
4121 print (
[email protected]749fbd92014-08-26 21:57:534122 'Your patch doesn\'t apply cleanly to ref \'%s\', '
4123 'the following files have merge conflicts:' % pending_ref)
[email protected]566a02a2014-08-22 01:34:134124 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
4125 print 'Please rebase your patch and try again.'
[email protected]749fbd92014-08-26 21:57:534126 RunGitWithCode(['cherry-pick', '--abort'])
[email protected]566a02a2014-08-22 01:34:134127 return code, out
4128
4129 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
[email protected]749fbd92014-08-26 21:57:534130 print 'Pushing commit to %s... It can take a while.' % pending_ref
[email protected]566a02a2014-08-22 01:34:134131 code, out = RunGitWithCode(
4132 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4133 if code == 0:
4134 # Success.
[email protected]e6896b52014-08-29 01:38:034135 print 'Commit pushed to pending ref successfully!'
[email protected]566a02a2014-08-22 01:34:134136 return code, out
4137
[email protected]749fbd92014-08-26 21:57:534138 print 'Push failed with exit code %d.' % code
4139 if out.strip():
4140 print out.strip()
4141 if IsFatalPushFailure(out):
4142 print (
4143 'Fatal push error. Make sure your .netrc credentials and git '
4144 'user.email are correct and you have push access to the repo.')
4145 return code, out
4146
4147 print 'All attempts to push to pending ref failed.'
[email protected]566a02a2014-08-22 01:34:134148 return code, out
4149
4150
[email protected]749fbd92014-08-26 21:57:534151def IsFatalPushFailure(push_stdout):
4152 """True if retrying push won't help."""
4153 return '(prohibited by Gerrit)' in push_stdout
4154
4155
[email protected]0633fb42013-08-16 20:06:144156@subcommand.usage('[upstream branch to apply against]')
[email protected]cc51cd02010-12-23 00:48:394157def CMDdcommit(parser, args):
[email protected]d9c1b202013-07-24 23:52:114158 """Commits the current changelist via git-svn."""
[email protected]cc51cd02010-12-23 00:48:394159 if not settings.GetIsGitSvn():
[email protected]09d7a6a2016-03-04 15:44:484160 if git_footers.get_footer_svn_id():
[email protected]f0e41522015-06-10 19:52:014161 # If it looks like previous commits were mirrored with git-svn.
4162 message = """This repository appears to be a git-svn mirror, but no
4163upstream SVN master is set. You probably need to run 'git auto-svn' once."""
4164 else:
4165 message = """This doesn't appear to be an SVN repository.
4166If your project has a true, writeable git repository, you probably want to run
4167'git cl land' instead.
4168If your project has a git mirror of an upstream SVN master, you probably need
4169to run 'git svn init'.
4170
4171Using the wrong command might cause your commit to appear to succeed, and the
4172review to be closed, without actually landing upstream. If you choose to
4173proceed, please verify that the commit lands upstream as expected."""
[email protected]cde3bb62011-01-20 01:16:144174 print(message)
[email protected]90541732011-04-01 17:54:184175 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
[email protected]cc51cd02010-12-23 00:48:394176 return SendUpstream(parser, args, 'dcommit')
4177
4178
[email protected]0633fb42013-08-16 20:06:144179@subcommand.usage('[upstream branch to apply against]')
[email protected]cee6dc42014-05-07 17:04:034180def CMDland(parser, args):
[email protected]d9c1b202013-07-24 23:52:114181 """Commits the current changelist via git."""
[email protected]09d7a6a2016-03-04 15:44:484182 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
[email protected]cc51cd02010-12-23 00:48:394183 print('This appears to be an SVN repository.')
4184 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
[email protected]f0e41522015-06-10 19:52:014185 print('(Ignore if this is the first commit after migrating from svn->git)')
[email protected]90541732011-04-01 17:54:184186 ask_for_data('[Press enter to push or ctrl-C to quit]')
[email protected]566a02a2014-08-22 01:34:134187 return SendUpstream(parser, args, 'land')
[email protected]cc51cd02010-12-23 00:48:394188
4189
[email protected]fbed6562015-09-25 21:22:364190@subcommand.usage('<patch url or issue id or issue url>')
[email protected]cc51cd02010-12-23 00:48:394191def CMDpatch(parser, args):
[email protected]e5e59002013-10-02 23:21:254192 """Patches in a code review."""
[email protected]cc51cd02010-12-23 00:48:394193 parser.add_option('-b', dest='newbranch',
4194 help='create a new branch off trunk for the patch')
[email protected]1ef44af2013-10-16 16:24:324195 parser.add_option('-f', '--force', action='store_true',
[email protected]cc51cd02010-12-23 00:48:394196 help='with -b, clobber any existing branch')
[email protected]1ef44af2013-10-16 16:24:324197 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4198 help='Change to the directory DIR immediately, '
[email protected]f86c7d32016-04-01 19:27:304199 'before doing anything else. Rietveld only.')
[email protected]1ef44af2013-10-16 16:24:324200 parser.add_option('--reject', action='store_true',
[email protected]6a0b07c2013-07-10 01:29:194201 help='failed patches spew .rej files rather than '
[email protected]f86c7d32016-04-01 19:27:304202 'attempting a 3-way merge. Rietveld only.')
[email protected]cc51cd02010-12-23 00:48:394203 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
[email protected]f86c7d32016-04-01 19:27:304204 help='don\'t commit after patch applies. Rietveld only.')
[email protected]1d88dd32016-02-04 16:25:124205
[email protected]f86c7d32016-04-01 19:27:304206
4207 group = optparse.OptionGroup(
4208 parser,
4209 'Options for continuing work on the current issue uploaded from a '
4210 'different clone (e.g. different machine). Must be used independently '
4211 'from the other options. No issue number should be specified, and the '
4212 'branch must have an issue number associated with it')
4213 group.add_option('--reapply', action='store_true', dest='reapply',
4214 help='Reset the branch and reapply the issue.\n'
4215 'CAUTION: This will undo any local changes in this '
4216 'branch')
[email protected]1d88dd32016-02-04 16:25:124217
4218 group.add_option('--pull', action='store_true', dest='pull',
[email protected]f86c7d32016-04-01 19:27:304219 help='Performs a pull before reapplying.')
[email protected]1d88dd32016-02-04 16:25:124220 parser.add_option_group(group)
4221
[email protected]cf6a5d22015-04-09 22:02:004222 auth.add_auth_options(parser)
[email protected]dde64622016-04-13 17:11:214223 _add_codereview_select_options(parser)
[email protected]cc51cd02010-12-23 00:48:394224 (options, args) = parser.parse_args(args)
[email protected]dde64622016-04-13 17:11:214225 _process_codereview_select_options(parser, options)
[email protected]cf6a5d22015-04-09 22:02:004226 auth_config = auth.extract_auth_config_from_options(options)
4227
[email protected]f86c7d32016-04-01 19:27:304228
[email protected]1d88dd32016-02-04 16:25:124229 if options.reapply :
[email protected]c2786d92016-05-31 19:53:504230 if options.newbranch:
4231 parser.error('--reapply works on the current branch only')
[email protected]1d88dd32016-02-04 16:25:124232 if len(args) > 0:
[email protected]c2786d92016-05-31 19:53:504233 parser.error('--reapply implies no additional arguments')
[email protected]fbed6562015-09-25 21:22:364234
[email protected]c2786d92016-05-31 19:53:504235 cl = Changelist(auth_config=auth_config,
4236 codereview=options.forced_codereview)
4237 if not cl.GetIssue():
4238 parser.error('current branch must have an associated issue')
4239
[email protected]1d88dd32016-02-04 16:25:124240 upstream = cl.GetUpstreamBranch()
4241 if upstream == None:
[email protected]f86c7d32016-04-01 19:27:304242 parser.error('No upstream branch specified. Cannot reset branch')
[email protected]1d88dd32016-02-04 16:25:124243
4244 RunGit(['reset', '--hard', upstream])
4245 if options.pull:
4246 RunGit(['pull'])
[email protected]1d88dd32016-02-04 16:25:124247
[email protected]c2786d92016-05-31 19:53:504248 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4249 options.directory)
4250
4251 if len(args) != 1 or not args[0]:
4252 parser.error('Must specify issue number or url')
4253
4254 # We don't want uncommitted changes mixed up with the patch.
4255 if git_common.is_dirty_git_tree('patch'):
[email protected]fbed6562015-09-25 21:22:364256 return 1
[email protected]cc51cd02010-12-23 00:48:394257
[email protected]c2786d92016-05-31 19:53:504258 if options.newbranch:
4259 if options.force:
4260 RunGit(['branch', '-D', options.newbranch],
4261 stderr=subprocess2.PIPE, error_ok=True)
4262 RunGit(['new-branch', options.newbranch])
4263
4264 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4265
[email protected]f86c7d32016-04-01 19:27:304266 if cl.IsGerrit():
4267 if options.reject:
4268 parser.error('--reject is not supported with Gerrit codereview.')
4269 if options.nocommit:
4270 parser.error('--nocommit is not supported with Gerrit codereview.')
4271 if options.directory:
4272 parser.error('--directory is not supported with Gerrit codereview.')
4273
[email protected]c2786d92016-05-31 19:53:504274 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
[email protected]f86c7d32016-04-01 19:27:304275 options.directory)
[email protected]cc51cd02010-12-23 00:48:394276
4277
4278def CMDrebase(parser, args):
[email protected]d9c1b202013-07-24 23:52:114279 """Rebases current branch on top of svn repo."""
[email protected]cc51cd02010-12-23 00:48:394280 # Provide a wrapper for git svn rebase to help avoid accidental
4281 # git svn dcommit.
4282 # It's the only command that doesn't use parser at all since we just defer
4283 # execution to git-svn.
[email protected]82b91cd2013-07-09 06:33:414284
[email protected]8b0553c2014-02-11 00:33:374285 return RunGitWithCode(['svn', 'rebase'] + args)[1]
[email protected]cc51cd02010-12-23 00:48:394286
4287
[email protected]3ec0d542014-01-14 20:00:034288def GetTreeStatus(url=None):
[email protected]cc51cd02010-12-23 00:48:394289 """Fetches the tree status and returns either 'open', 'closed',
4290 'unknown' or 'unset'."""
[email protected]3ec0d542014-01-14 20:00:034291 url = url or settings.GetTreeStatusUrl(error_ok=True)
[email protected]cc51cd02010-12-23 00:48:394292 if url:
4293 status = urllib2.urlopen(url).read().lower()
4294 if status.find('closed') != -1 or status == '0':
4295 return 'closed'
4296 elif status.find('open') != -1 or status == '1':
4297 return 'open'
4298 return 'unknown'
[email protected]cc51cd02010-12-23 00:48:394299 return 'unset'
4300
[email protected]970c5222011-03-12 00:32:244301
[email protected]cc51cd02010-12-23 00:48:394302def GetTreeStatusReason():
4303 """Fetches the tree status from a json url and returns the message
4304 with the reason for the tree to be opened or closed."""
[email protected]bf1a7ba2011-02-01 16:21:464305 url = settings.GetTreeStatusUrl()
4306 json_url = urlparse.urljoin(url, '/current?format=json')
[email protected]cc51cd02010-12-23 00:48:394307 connection = urllib2.urlopen(json_url)
4308 status = json.loads(connection.read())
4309 connection.close()
4310 return status['message']
4311
[email protected]970c5222011-03-12 00:32:244312
[email protected]2b34d552014-08-14 22:18:424313def GetBuilderMaster(bot_list):
4314 """For a given builder, fetch the master from AE if available."""
4315 map_url = 'https://builders-map.appspot.com/'
4316 try:
4317 master_map = json.load(urllib2.urlopen(map_url))
4318 except urllib2.URLError as e:
4319 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4320 (map_url, e))
4321 except ValueError as e:
4322 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4323 if not master_map:
4324 return None, 'Failed to build master map.'
4325
4326 result_master = ''
4327 for bot in bot_list:
4328 builder = bot.split(':', 1)[0]
4329 master_list = master_map.get(builder, [])
4330 if not master_list:
4331 return None, ('No matching master for builder %s.' % builder)
4332 elif len(master_list) > 1:
4333 return None, ('The builder name %s exists in multiple masters %s.' %
4334 (builder, master_list))
4335 else:
4336 cur_master = master_list[0]
4337 if not result_master:
4338 result_master = cur_master
4339 elif result_master != cur_master:
4340 return None, 'The builders do not belong to the same master.'
4341 return result_master, None
4342
4343
[email protected]cc51cd02010-12-23 00:48:394344def CMDtree(parser, args):
[email protected]d9c1b202013-07-24 23:52:114345 """Shows the status of the tree."""
[email protected]97ae58e2011-03-18 00:29:204346 _, args = parser.parse_args(args)
[email protected]cc51cd02010-12-23 00:48:394347 status = GetTreeStatus()
4348 if 'unset' == status:
4349 print 'You must configure your tree status URL by running "git cl config".'
4350 return 2
4351
4352 print "The tree is %s" % status
4353 print
4354 print GetTreeStatusReason()
4355 if status != 'open':
4356 return 1
4357 return 0
4358
4359
[email protected]15192402012-09-06 12:38:294360def CMDtry(parser, args):
[email protected]fa330e82016-04-13 17:09:524361 """Triggers try jobs through BuildBucket."""
[email protected]15192402012-09-06 12:38:294362 group = optparse.OptionGroup(parser, "Try job options")
4363 group.add_option(
4364 "-b", "--bot", action="append",
4365 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4366 "times to specify multiple builders. ex: "
[email protected]52914132015-01-22 10:37:094367 "'-b win_rel -b win_layout'. See "
[email protected]15192402012-09-06 12:38:294368 "the try server waterfall for the builders name and the tests "
[email protected]52914132015-01-22 10:37:094369 "available."))
[email protected]15192402012-09-06 12:38:294370 group.add_option(
[email protected]58a69cb2014-03-01 02:08:294371 "-m", "--master", default='',
[email protected]9e849272014-04-04 00:31:554372 help=("Specify a try master where to run the tries."))
[email protected]feb9e2a2015-09-25 19:11:094373 group.add_option( "--luci", action='store_true')
[email protected]58a69cb2014-03-01 02:08:294374 group.add_option(
[email protected]15192402012-09-06 12:38:294375 "-r", "--revision",
4376 help="Revision to use for the try job; default: the "
4377 "revision will be determined by the try server; see "
4378 "its waterfall for more info")
4379 group.add_option(
4380 "-c", "--clobber", action="store_true", default=False,
4381 help="Force a clobber before building; e.g. don't do an "
4382 "incremental build")
4383 group.add_option(
4384 "--project",
4385 help="Override which project to use. Projects are defined "
4386 "server-side to define what default bot set to use")
4387 group.add_option(
[email protected]45453142015-09-15 08:45:224388 "-p", "--property", dest="properties", action="append", default=[],
4389 help="Specify generic properties in the form -p key1=value1 -p "
4390 "key2=value2 etc (buildbucket only). The value will be treated as "
4391 "json if decodable, or as string otherwise.")
4392 group.add_option(
[email protected]15192402012-09-06 12:38:294393 "-n", "--name", help="Try job name; default to current branch name")
[email protected]6ebaf782015-05-12 19:17:544394 group.add_option(
[email protected]db375572015-08-17 19:22:234395 "--use-rietveld", action="store_true", default=False,
4396 help="Use Rietveld to trigger try jobs.")
4397 group.add_option(
4398 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4399 help="Host of buildbucket. The default host is %default.")
[email protected]15192402012-09-06 12:38:294400 parser.add_option_group(group)
[email protected]cf6a5d22015-04-09 22:02:004401 auth.add_auth_options(parser)
[email protected]15192402012-09-06 12:38:294402 options, args = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:004403 auth_config = auth.extract_auth_config_from_options(options)
[email protected]15192402012-09-06 12:38:294404
[email protected]45453142015-09-15 08:45:224405 if options.use_rietveld and options.properties:
4406 parser.error('Properties can only be specified with buildbucket')
4407
4408 # Make sure that all properties are prop=value pairs.
4409 bad_params = [x for x in options.properties if '=' not in x]
4410 if bad_params:
4411 parser.error('Got properties with missing "=": %s' % bad_params)
4412
[email protected]15192402012-09-06 12:38:294413 if args:
4414 parser.error('Unknown arguments: %s' % args)
4415
[email protected]cf6a5d22015-04-09 22:02:004416 cl = Changelist(auth_config=auth_config)
[email protected]15192402012-09-06 12:38:294417 if not cl.GetIssue():
4418 parser.error('Need to upload first')
4419
[email protected]fa330e82016-04-13 17:09:524420 if cl.IsGerrit():
4421 parser.error(
4422 'Not yet supported for Gerrit (http://crbug.com/599931).\n'
4423 'If your project has Commit Queue, dry run is a workaround:\n'
4424 ' git cl set-commit --dry-run')
4425 # Code below assumes Rietveld issue.
4426 # TODO(tandrii): actually implement for Gerrit http://crbug.com/599931.
4427
[email protected]16f10f72014-06-24 22:14:364428 props = cl.GetIssueProperties()
[email protected]787e3062014-08-20 16:31:194429 if props.get('closed'):
4430 parser.error('Cannot send tryjobs for a closed CL')
4431
[email protected]16f10f72014-06-24 22:14:364432 if props.get('private'):
4433 parser.error('Cannot use trybots with private issue')
4434
[email protected]15192402012-09-06 12:38:294435 if not options.name:
4436 options.name = cl.GetBranch()
4437
[email protected]8da7f272014-03-14 01:28:394438 if options.bot and not options.master:
[email protected]2b34d552014-08-14 22:18:424439 options.master, err_msg = GetBuilderMaster(options.bot)
4440 if err_msg:
4441 parser.error('Tryserver master cannot be found because: %s\n'
4442 'Please manually specify the tryserver master'
4443 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
[email protected]8da7f272014-03-14 01:28:394444
[email protected]58a69cb2014-03-01 02:08:294445 def GetMasterMap():
[email protected]52914132015-01-22 10:37:094446 # Process --bot.
[email protected]58a69cb2014-03-01 02:08:294447 if not options.bot:
4448 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
[email protected]15192402012-09-06 12:38:294449
[email protected]58a69cb2014-03-01 02:08:294450 # Get try masters from PRESUBMIT.py files.
4451 masters = presubmit_support.DoGetTryMasters(
4452 change,
4453 change.LocalPaths(),
4454 settings.GetRoot(),
4455 None,
4456 None,
4457 options.verbose,
4458 sys.stdout)
4459 if masters:
4460 return masters
[email protected]43064fd2013-12-18 20:07:444461
[email protected]58a69cb2014-03-01 02:08:294462 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4463 options.bot = presubmit_support.DoGetTrySlaves(
4464 change,
4465 change.LocalPaths(),
4466 settings.GetRoot(),
4467 None,
4468 None,
4469 options.verbose,
4470 sys.stdout)
[email protected]71184c02016-01-13 15:18:444471
4472 if not options.bot:
4473 # Get try masters from cq.cfg if any.
4474 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4475 # location.
4476 cq_cfg = os.path.join(change.RepositoryRoot(),
4477 'infra', 'config', 'cq.cfg')
4478 if os.path.exists(cq_cfg):
4479 masters = {}
[email protected]59994802016-01-14 10:10:334480 cq_masters = commit_queue.get_master_builder_map(
4481 cq_cfg, include_experimental=False, include_triggered=False)
[email protected]71184c02016-01-13 15:18:444482 for master, builders in cq_masters.iteritems():
4483 for builder in builders:
4484 # Skip presubmit builders, because these will fail without LGTM.
[email protected]2403e802016-04-29 12:34:424485 masters.setdefault(master, {})[builder] = ['defaulttests']
[email protected]71184c02016-01-13 15:18:444486 if masters:
tandriib93dd2b2016-06-07 15:03:084487 print('Loaded default bots from CQ config (%s)' % cq_cfg)
[email protected]71184c02016-01-13 15:18:444488 return masters
tandriib93dd2b2016-06-07 15:03:084489 else:
4490 print('CQ config exists (%s) but has no try bots listed' % cq_cfg)
[email protected]71184c02016-01-13 15:18:444491
[email protected]58a69cb2014-03-01 02:08:294492 if not options.bot:
4493 parser.error('No default try builder to try, use --bot')
[email protected]15192402012-09-06 12:38:294494
[email protected]58a69cb2014-03-01 02:08:294495 builders_and_tests = {}
4496 # TODO(machenbach): The old style command-line options don't support
4497 # multiple try masters yet.
4498 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4499 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4500
4501 for bot in old_style:
4502 if ':' in bot:
[email protected]52914132015-01-22 10:37:094503 parser.error('Specifying testfilter is no longer supported')
[email protected]58a69cb2014-03-01 02:08:294504 elif ',' in bot:
4505 parser.error('Specify one bot per --bot flag')
4506 else:
[email protected]3764fa22015-10-21 16:40:404507 builders_and_tests.setdefault(bot, [])
[email protected]58a69cb2014-03-01 02:08:294508
4509 for bot, tests in new_style:
4510 builders_and_tests.setdefault(bot, []).extend(tests)
4511
4512 # Return a master map with one master to be backwards compatible. The
4513 # master name defaults to an empty string, which will cause the master
4514 # not to be set on rietveld (deprecated).
4515 return {options.master: builders_and_tests}
4516
4517 masters = GetMasterMap()
[email protected]43064fd2013-12-18 20:07:444518
[email protected]58a69cb2014-03-01 02:08:294519 for builders in masters.itervalues():
4520 if any('triggered' in b for b in builders):
4521 print >> sys.stderr, (
4522 'ERROR You are trying to send a job to a triggered bot. This type of'
4523 ' bot requires an\ninitial job from a parent (usually a builder). '
4524 'Instead send your job to the parent.\n'
4525 'Bot list: %s' % builders)
4526 return 1
[email protected]f3b21232012-09-24 20:48:554527
[email protected]36e420b2013-08-06 23:21:124528 patchset = cl.GetMostRecentPatchset()
4529 if patchset and patchset != cl.GetPatchset():
4530 print(
4531 '\nWARNING Mismatch between local config and server. Did a previous '
4532 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4533 'Continuing using\npatchset %s.\n' % patchset)
[email protected]feb9e2a2015-09-25 19:11:094534 if options.luci:
4535 trigger_luci_job(cl, masters, options)
4536 elif not options.use_rietveld:
[email protected]6ebaf782015-05-12 19:17:544537 try:
4538 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4539 except BuildbucketResponseException as ex:
4540 print 'ERROR: %s' % ex
[email protected]d246c972013-12-21 22:47:384541 return 1
[email protected]6ebaf782015-05-12 19:17:544542 except Exception as e:
4543 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4544 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4545 e, stacktrace)
4546 return 1
4547 else:
4548 try:
4549 cl.RpcServer().trigger_distributed_try_jobs(
4550 cl.GetIssue(), patchset, options.name, options.clobber,
4551 options.revision, masters)
4552 except urllib2.HTTPError as e:
4553 if e.code == 404:
4554 print('404 from rietveld; '
4555 'did you mean to use "git try" instead of "git cl try"?')
4556 return 1
4557 print('Tried jobs on:')
[email protected]58a69cb2014-03-01 02:08:294558
[email protected]6ebaf782015-05-12 19:17:544559 for (master, builders) in sorted(masters.iteritems()):
4560 if master:
4561 print 'Master: %s' % master
4562 length = max(len(builder) for builder in builders)
4563 for builder in sorted(builders):
4564 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
[email protected]15192402012-09-06 12:38:294565 return 0
4566
4567
[email protected]b015fac2016-02-26 14:52:014568def CMDtry_results(parser, args):
4569 group = optparse.OptionGroup(parser, "Try job results options")
4570 group.add_option(
4571 "-p", "--patchset", type=int, help="patchset number if not current.")
4572 group.add_option(
[email protected]6cf98c82016-03-15 11:56:004573 "--print-master", action='store_true', help="print master name as well.")
4574 group.add_option(
[email protected]596cd5c2016-04-04 21:34:394575 "--color", action='store_true', default=setup_color.IS_TTY,
[email protected]6cf98c82016-03-15 11:56:004576 help="force color output, useful when piping output.")
[email protected]b015fac2016-02-26 14:52:014577 group.add_option(
4578 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4579 help="Host of buildbucket. The default host is %default.")
4580 parser.add_option_group(group)
4581 auth.add_auth_options(parser)
4582 options, args = parser.parse_args(args)
4583 if args:
4584 parser.error('Unrecognized args: %s' % ' '.join(args))
4585
4586 auth_config = auth.extract_auth_config_from_options(options)
4587 cl = Changelist(auth_config=auth_config)
4588 if not cl.GetIssue():
4589 parser.error('Need to upload first')
4590
4591 if not options.patchset:
4592 options.patchset = cl.GetMostRecentPatchset()
4593 if options.patchset and options.patchset != cl.GetPatchset():
4594 print(
4595 '\nWARNING Mismatch between local config and server. Did a previous '
4596 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4597 'Continuing using\npatchset %s.\n' % options.patchset)
4598 try:
4599 jobs = fetch_try_jobs(auth_config, cl, options)
4600 except BuildbucketResponseException as ex:
4601 print 'Buildbucket error: %s' % ex
4602 return 1
4603 except Exception as e:
4604 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4605 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4606 e, stacktrace)
4607 return 1
4608 print_tryjobs(options, jobs)
4609 return 0
4610
4611
[email protected]0633fb42013-08-16 20:06:144612@subcommand.usage('[new upstream branch]')
[email protected]cc51cd02010-12-23 00:48:394613def CMDupstream(parser, args):
[email protected]d9c1b202013-07-24 23:52:114614 """Prints or sets the name of the upstream branch, if any."""
[email protected]97ae58e2011-03-18 00:29:204615 _, args = parser.parse_args(args)
[email protected]ac0ba332012-08-09 23:42:534616 if len(args) > 1:
[email protected]27bb3872011-05-30 20:33:194617 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]ac0ba332012-08-09 23:42:534618
[email protected]cc51cd02010-12-23 00:48:394619 cl = Changelist()
[email protected]ac0ba332012-08-09 23:42:534620 if args:
4621 # One arg means set upstream branch.
[email protected]c9cf90a2014-04-28 20:32:314622 branch = cl.GetBranch()
4623 RunGit(['branch', '--set-upstream', branch, args[0]])
[email protected]ac0ba332012-08-09 23:42:534624 cl = Changelist()
4625 print "Upstream branch set to " + cl.GetUpstreamBranch()
[email protected]c9cf90a2014-04-28 20:32:314626
4627 # Clear configured merge-base, if there is one.
4628 git_common.remove_merge_base(branch)
[email protected]ac0ba332012-08-09 23:42:534629 else:
4630 print cl.GetUpstreamBranch()
[email protected]cc51cd02010-12-23 00:48:394631 return 0
4632
4633
[email protected]00858c82013-12-02 23:08:034634def CMDweb(parser, args):
4635 """Opens the current CL in the web browser."""
4636 _, args = parser.parse_args(args)
4637 if args:
4638 parser.error('Unrecognized args: %s' % ' '.join(args))
4639
4640 issue_url = Changelist().GetIssueURL()
4641 if not issue_url:
4642 print >> sys.stderr, 'ERROR No issue to open'
4643 return 1
4644
4645 webbrowser.open(issue_url)
4646 return 0
4647
4648
[email protected]27bb3872011-05-30 20:33:194649def CMDset_commit(parser, args):
[email protected]d9c1b202013-07-24 23:52:114650 """Sets the commit bit to trigger the Commit Queue."""
[email protected]fa330e82016-04-13 17:09:524651 parser.add_option('-d', '--dry-run', action='store_true',
4652 help='trigger in dry run mode')
4653 parser.add_option('-c', '--clear', action='store_true',
4654 help='stop CQ run, if any')
[email protected]cf6a5d22015-04-09 22:02:004655 auth.add_auth_options(parser)
4656 options, args = parser.parse_args(args)
4657 auth_config = auth.extract_auth_config_from_options(options)
[email protected]27bb3872011-05-30 20:33:194658 if args:
4659 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]fa330e82016-04-13 17:09:524660 if options.dry_run and options.clear:
4661 parser.error('Make up your mind: both --dry-run and --clear not allowed')
4662
[email protected]cf6a5d22015-04-09 22:02:004663 cl = Changelist(auth_config=auth_config)
[email protected]fa330e82016-04-13 17:09:524664 if options.clear:
4665 state = _CQState.CLEAR
4666 elif options.dry_run:
4667 state = _CQState.DRY_RUN
4668 else:
4669 state = _CQState.COMMIT
4670 if not cl.GetIssue():
4671 parser.error('Must upload the issue first')
4672 cl.SetCQState(state)
[email protected]27bb3872011-05-30 20:33:194673 return 0
4674
4675
[email protected]411034a2013-02-26 15:12:014676def CMDset_close(parser, args):
[email protected]d9c1b202013-07-24 23:52:114677 """Closes the issue."""
[email protected]cf6a5d22015-04-09 22:02:004678 auth.add_auth_options(parser)
4679 options, args = parser.parse_args(args)
4680 auth_config = auth.extract_auth_config_from_options(options)
[email protected]411034a2013-02-26 15:12:014681 if args:
4682 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]cf6a5d22015-04-09 22:02:004683 cl = Changelist(auth_config=auth_config)
[email protected]411034a2013-02-26 15:12:014684 # Ensure there actually is an issue to close.
4685 cl.GetDescription()
4686 cl.CloseIssue()
4687 return 0
4688
4689
[email protected]87b9bf02013-09-26 20:35:154690def CMDdiff(parser, args):
[email protected]37b2ec02015-04-03 00:49:154691 """Shows differences between local tree and last upload."""
[email protected]cf6a5d22015-04-09 22:02:004692 auth.add_auth_options(parser)
4693 options, args = parser.parse_args(args)
4694 auth_config = auth.extract_auth_config_from_options(options)
4695 if args:
4696 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]46309bf2015-04-03 21:04:494697
4698 # Uncommitted (staged and unstaged) changes will be destroyed by
[email protected]f86c7d32016-04-01 19:27:304699 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
[email protected]46309bf2015-04-03 21:04:494700 # Staged changes would be committed along with the patch from last
4701 # upload, hence counted toward the "last upload" side in the final
4702 # diff output, and this is not what we want.
[email protected]71437c02015-04-09 19:29:404703 if git_common.is_dirty_git_tree('diff'):
[email protected]46309bf2015-04-03 21:04:494704 return 1
4705
[email protected]cf6a5d22015-04-09 22:02:004706 cl = Changelist(auth_config=auth_config)
[email protected]78dc9842013-11-25 18:43:444707 issue = cl.GetIssue()
[email protected]87b9bf02013-09-26 20:35:154708 branch = cl.GetBranch()
[email protected]78dc9842013-11-25 18:43:444709 if not issue:
4710 DieWithError('No issue found for current branch (%s)' % branch)
[email protected]87b9bf02013-09-26 20:35:154711 TMP_BRANCH = 'git-cl-diff'
[email protected]8b0553c2014-02-11 00:33:374712 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]87b9bf02013-09-26 20:35:154713
4714 # Create a new branch based on the merge-base
4715 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
[email protected]534f67a2016-04-07 18:47:054716 # Clear cached branch in cl object, to avoid overwriting original CL branch
4717 # properties.
4718 cl.ClearBranch()
[email protected]87b9bf02013-09-26 20:35:154719 try:
[email protected]f86c7d32016-04-01 19:27:304720 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
[email protected]87b9bf02013-09-26 20:35:154721 if rtn != 0:
[email protected]a872e752015-04-28 23:42:184722 RunGit(['reset', '--hard'])
[email protected]87b9bf02013-09-26 20:35:154723 return rtn
4724
[email protected]06928532015-02-03 02:11:294725 # Switch back to starting branch and diff against the temporary
[email protected]87b9bf02013-09-26 20:35:154726 # branch containing the latest rietveld patch.
[email protected]06928532015-02-03 02:11:294727 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
[email protected]87b9bf02013-09-26 20:35:154728 finally:
4729 RunGit(['checkout', '-q', branch])
4730 RunGit(['branch', '-D', TMP_BRANCH])
4731
4732 return 0
4733
4734
[email protected]faf3fdf2013-09-20 02:11:484735def CMDowners(parser, args):
[email protected]37b2ec02015-04-03 00:49:154736 """Interactively find the owners for reviewing."""
[email protected]faf3fdf2013-09-20 02:11:484737 parser.add_option(
4738 '--no-color',
4739 action='store_true',
4740 help='Use this option to disable color output')
[email protected]cf6a5d22015-04-09 22:02:004741 auth.add_auth_options(parser)
[email protected]faf3fdf2013-09-20 02:11:484742 options, args = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:004743 auth_config = auth.extract_auth_config_from_options(options)
[email protected]faf3fdf2013-09-20 02:11:484744
4745 author = RunGit(['config', 'user.email']).strip() or None
4746
[email protected]cf6a5d22015-04-09 22:02:004747 cl = Changelist(auth_config=auth_config)
[email protected]faf3fdf2013-09-20 02:11:484748
4749 if args:
4750 if len(args) > 1:
4751 parser.error('Unknown args')
4752 base_branch = args[0]
4753 else:
4754 # Default to diffing against the common ancestor of the upstream branch.
[email protected]8b0553c2014-02-11 00:33:374755 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]faf3fdf2013-09-20 02:11:484756
4757 change = cl.GetChange(base_branch, None)
4758 return owners_finder.OwnersFinder(
4759 [f.LocalPath() for f in
4760 cl.GetChange(base_branch, None).AffectedFiles()],
4761 change.RepositoryRoot(), author,
4762 fopen=file, os_path=os.path, glob=glob.glob,
4763 disable_color=options.no_color).run()
4764
4765
[email protected]6f7fa5e2016-01-20 19:32:214766def BuildGitDiffCmd(diff_type, upstream_commit, args):
[email protected]e0a7c5d2015-02-23 20:30:084767 """Generates a diff command."""
4768 # Generate diff for the current branch's changes.
4769 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4770 upstream_commit, '--' ]
4771
4772 if args:
4773 for arg in args:
[email protected]6f7fa5e2016-01-20 19:32:214774 if os.path.isdir(arg) or os.path.isfile(arg):
[email protected]e0a7c5d2015-02-23 20:30:084775 diff_cmd.append(arg)
4776 else:
4777 DieWithError('Argument "%s" is not a file or a directory' % arg)
[email protected]e0a7c5d2015-02-23 20:30:084778
4779 return diff_cmd
4780
[email protected]6f7fa5e2016-01-20 19:32:214781def MatchingFileType(file_name, extensions):
4782 """Returns true if the file name ends with one of the given extensions."""
4783 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
[email protected]e0a7c5d2015-02-23 20:30:084784
[email protected]555cfe42014-01-29 18:21:394785@subcommand.usage('[files or directories to diff]')
[email protected]fab8f822013-05-06 17:43:094786def CMDformat(parser, args):
[email protected]9d0644d2015-06-05 23:16:544787 """Runs auto-formatting tools (clang-format etc.) on the diff."""
[email protected]9819b1b2014-12-09 21:21:534788 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
[email protected]8b61f112016-02-05 13:28:584789 GN_EXTS = ['.gn', '.gni']
[email protected]3b7e15c2014-01-21 17:44:474790 parser.add_option('--full', action='store_true',
4791 help='Reformat the full content of all touched files')
4792 parser.add_option('--dry-run', action='store_true',
4793 help='Don\'t modify any file on disk.')
[email protected]9d0644d2015-06-05 23:16:544794 parser.add_option('--python', action='store_true',
4795 help='Format python code with yapf (experimental).')
[email protected]04d5a222014-03-07 18:30:424796 parser.add_option('--diff', action='store_true',
4797 help='Print diff to stdout rather than modifying files.')
[email protected]fab8f822013-05-06 17:43:094798 opts, args = parser.parse_args(args)
[email protected]fab8f822013-05-06 17:43:094799
[email protected]ff7a1fb2013-12-10 19:21:414800 # git diff generates paths against the root of the repository. Change
4801 # to that directory so clang-format can find files even within subdirs.
[email protected]8b0553c2014-02-11 00:33:374802 rel_base_path = settings.GetRelativeRoot()
[email protected]ff7a1fb2013-12-10 19:21:414803 if rel_base_path:
4804 os.chdir(rel_base_path)
4805
[email protected]29e47272013-05-17 17:01:464806 # Grab the merge-base commit, i.e. the upstream commit of the current
4807 # branch when it was created or the last time it was rebased. This is
4808 # to cover the case where the user may have called "git fetch origin",
4809 # moving the origin branch to a newer commit, but hasn't rebased yet.
4810 upstream_commit = None
4811 cl = Changelist()
4812 upstream_branch = cl.GetUpstreamBranch()
4813 if upstream_branch:
4814 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4815 upstream_commit = upstream_commit.strip()
4816
4817 if not upstream_commit:
4818 DieWithError('Could not find base commit for this branch. '
4819 'Are you in detached state?')
4820
[email protected]6f7fa5e2016-01-20 19:32:214821 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4822 diff_output = RunGit(changed_files_cmd)
4823 diff_files = diff_output.splitlines()
[email protected]ad21b922016-01-28 17:48:424824 # Filter out files deleted by this CL
4825 diff_files = [x for x in diff_files if os.path.isfile(x)]
[email protected]e0a7c5d2015-02-23 20:30:084826
[email protected]6f7fa5e2016-01-20 19:32:214827 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4828 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4829 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
[email protected]8b61f112016-02-05 13:28:584830 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
[email protected]29e47272013-05-17 17:01:464831
[email protected]3ac1c4e2014-01-16 02:44:424832 top_dir = os.path.normpath(
4833 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4834
[email protected]e0a7c5d2015-02-23 20:30:084835 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4836 # formatted. This is used to block during the presubmit.
4837 return_value = 0
4838
[email protected]0b35f5d2016-02-25 22:39:234839 if clang_diff_files:
[email protected]5573df12016-04-12 18:34:104840 # Locate the clang-format binary in the checkout
4841 try:
4842 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4843 except clang_format.NotFoundError, e:
4844 DieWithError(e)
4845
[email protected]0b35f5d2016-02-25 22:39:234846 if opts.full:
[email protected]e0a7c5d2015-02-23 20:30:084847 cmd = [clang_format_tool]
4848 if not opts.dry_run and not opts.diff:
4849 cmd.append('-i')
[email protected]6f7fa5e2016-01-20 19:32:214850 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
[email protected]e0a7c5d2015-02-23 20:30:084851 if opts.diff:
4852 sys.stdout.write(stdout)
[email protected]0b35f5d2016-02-25 22:39:234853 else:
4854 env = os.environ.copy()
4855 env['PATH'] = str(os.path.dirname(clang_format_tool))
4856 try:
4857 script = clang_format.FindClangFormatScriptInChromiumTree(
4858 'clang-format-diff.py')
4859 except clang_format.NotFoundError, e:
4860 DieWithError(e)
[email protected]d6ddc1c2013-10-25 15:36:324861
[email protected]0b35f5d2016-02-25 22:39:234862 cmd = [sys.executable, script, '-p0']
4863 if not opts.dry_run and not opts.diff:
4864 cmd.append('-i')
[email protected]d6ddc1c2013-10-25 15:36:324865
[email protected]0b35f5d2016-02-25 22:39:234866 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4867 diff_output = RunGit(diff_cmd)
[email protected]6f7fa5e2016-01-20 19:32:214868
[email protected]0b35f5d2016-02-25 22:39:234869 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4870 if opts.diff:
4871 sys.stdout.write(stdout)
4872 if opts.dry_run and len(stdout) > 0:
4873 return_value = 2
[email protected]fab8f822013-05-06 17:43:094874
[email protected]9d0644d2015-06-05 23:16:544875 # Similar code to above, but using yapf on .py files rather than clang-format
4876 # on C/C++ files
4877 if opts.python:
[email protected]9d0644d2015-06-05 23:16:544878 yapf_tool = gclient_utils.FindExecutable('yapf')
4879 if yapf_tool is None:
4880 DieWithError('yapf not found in PATH')
4881
4882 if opts.full:
[email protected]6f7fa5e2016-01-20 19:32:214883 if python_diff_files:
[email protected]9d0644d2015-06-05 23:16:544884 cmd = [yapf_tool]
4885 if not opts.dry_run and not opts.diff:
4886 cmd.append('-i')
[email protected]6f7fa5e2016-01-20 19:32:214887 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
[email protected]9d0644d2015-06-05 23:16:544888 if opts.diff:
4889 sys.stdout.write(stdout)
4890 else:
4891 # TODO(sbc): yapf --lines mode still has some issues.
4892 # https://github.com/google/yapf/issues/154
4893 DieWithError('--python currently only works with --full')
4894
[email protected]6f7fa5e2016-01-20 19:32:214895 # Dart's formatter does not have the nice property of only operating on
4896 # modified chunks, so hard code full.
4897 if dart_diff_files:
[email protected]e0a7c5d2015-02-23 20:30:084898 try:
4899 command = [dart_format.FindDartFmtToolInChromiumTree()]
4900 if not opts.dry_run and not opts.diff:
4901 command.append('-w')
[email protected]6f7fa5e2016-01-20 19:32:214902 command.extend(dart_diff_files)
[email protected]e0a7c5d2015-02-23 20:30:084903
[email protected]6593d932016-03-03 15:41:154904 stdout = RunCommand(command, cwd=top_dir)
[email protected]e0a7c5d2015-02-23 20:30:084905 if opts.dry_run and stdout:
4906 return_value = 2
4907 except dart_format.NotFoundError as e:
[email protected]3e445022015-12-17 09:07:264908 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4909 'found in this checkout. Files in other languages are still ' +
4910 'formatted.')
[email protected]e0a7c5d2015-02-23 20:30:084911
[email protected]8b61f112016-02-05 13:28:584912 # Format GN build files. Always run on full build files for canonical form.
4913 if gn_diff_files:
4914 cmd = ['gn', 'format']
4915 if not opts.dry_run and not opts.diff:
4916 cmd.append('--in-place')
4917 for gn_diff_file in gn_diff_files:
[email protected]627d9002016-04-29 00:00:524918 stdout = RunCommand(cmd + [gn_diff_file],
4919 shell=sys.platform == 'win32',
4920 cwd=top_dir)
[email protected]8b61f112016-02-05 13:28:584921 if opts.diff:
4922 sys.stdout.write(stdout)
4923
[email protected]e0a7c5d2015-02-23 20:30:084924 return return_value
[email protected]fab8f822013-05-06 17:43:094925
4926
[email protected]84a80c42015-09-22 20:40:374927@subcommand.usage('<codereview url or issue id>')
4928def CMDcheckout(parser, args):
[email protected]5df290f2016-04-11 16:12:294929 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
[email protected]84a80c42015-09-22 20:40:374930 _, args = parser.parse_args(args)
4931
4932 if len(args) != 1:
4933 parser.print_help()
4934 return 1
4935
[email protected]f86c7d32016-04-01 19:27:304936 issue_arg = ParseIssueNumberArgument(args[0])
[email protected]de6c9a12016-04-11 15:33:534937 if not issue_arg.valid:
[email protected]84a80c42015-09-22 20:40:374938 parser.print_help()
4939 return 1
[email protected]abd27e52016-04-11 15:43:324940 target_issue = str(issue_arg.issue)
[email protected]84a80c42015-09-22 20:40:374941
[email protected]5df290f2016-04-11 16:12:294942 def find_issues(issueprefix):
[email protected]26c8fd22016-04-11 21:33:214943 output = RunGit(['config', '--local', '--get-regexp',
4944 r'branch\..*\.%s' % issueprefix],
4945 error_ok=True)
4946 for key, issue in [x.split() for x in output.splitlines()]:
[email protected]5df290f2016-04-11 16:12:294947 if issue == target_issue:
4948 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
[email protected]84a80c42015-09-22 20:40:374949
[email protected]5df290f2016-04-11 16:12:294950 branches = []
4951 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
[email protected]d03bc632016-04-12 14:17:264952 branches.extend(find_issues(cls.IssueSettingSuffix()))
[email protected]84a80c42015-09-22 20:40:374953 if len(branches) == 0:
4954 print 'No branch found for issue %s.' % target_issue
4955 return 1
4956 if len(branches) == 1:
4957 RunGit(['checkout', branches[0]])
4958 else:
4959 print 'Multiple branches match issue %s:' % target_issue
4960 for i in range(len(branches)):
4961 print '%d: %s' % (i, branches[i])
4962 which = raw_input('Choose by index: ')
4963 try:
4964 RunGit(['checkout', branches[int(which)]])
4965 except (IndexError, ValueError):
4966 print 'Invalid selection, not checking out any branch.'
4967 return 1
4968
4969 return 0
4970
4971
[email protected]29404b52014-09-08 22:58:004972def CMDlol(parser, args):
4973 # This command is intentionally undocumented.
[email protected]3421c992014-11-02 02:20:324974 print zlib.decompress(base64.b64decode(
4975 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4976 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4977 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4978 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
[email protected]29404b52014-09-08 22:58:004979 return 0
4980
4981
[email protected]d9c1b202013-07-24 23:52:114982class OptionParser(optparse.OptionParser):
4983 """Creates the option parse and add --verbose support."""
4984 def __init__(self, *args, **kwargs):
[email protected]0633fb42013-08-16 20:06:144985 optparse.OptionParser.__init__(
4986 self, *args, prog='git cl', version=__version__, **kwargs)
[email protected]d9c1b202013-07-24 23:52:114987 self.add_option(
4988 '-v', '--verbose', action='count', default=0,
4989 help='Use 2 times for more debugging info')
4990
4991 def parse_args(self, args=None, values=None):
4992 options, args = optparse.OptionParser.parse_args(self, args, values)
4993 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4994 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4995 return options, args
4996
[email protected]d9c1b202013-07-24 23:52:114997
[email protected]cc51cd02010-12-23 00:48:394998def main(argv):
[email protected]82798cb2012-02-23 18:16:124999 if sys.hexversion < 0x02060000:
5000 print >> sys.stderr, (
5001 '\nYour python version %s is unsupported, please upgrade.\n' %
5002 sys.version.split(' ', 1)[0])
5003 return 2
[email protected]2e23ce32013-05-07 12:42:285004
[email protected]ddd59412011-11-30 14:20:385005 # Reload settings.
5006 global settings
5007 settings = Settings()
5008
[email protected]39c0b222013-08-17 16:57:015009 colorize_CMDstatus_doc()
[email protected]0633fb42013-08-16 20:06:145010 dispatcher = subcommand.CommandDispatcher(__name__)
5011 try:
5012 return dispatcher.execute(OptionParser(), argv)
[email protected]eed4df32015-04-10 21:30:205013 except auth.AuthenticationError as e:
5014 DieWithError(str(e))
[email protected]0633fb42013-08-16 20:06:145015 except urllib2.HTTPError, e:
5016 if e.code != 500:
5017 raise
5018 DieWithError(
5019 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5020 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
[email protected]013731e2015-02-26 18:28:435021 return 0
[email protected]cc51cd02010-12-23 00:48:395022
5023
5024if __name__ == '__main__':
[email protected]2e23ce32013-05-07 12:42:285025 # These affect sys.stdout so do it outside of main() to simplify mocks in
5026 # unit testing.
[email protected]6f09cd92011-04-01 16:38:125027 fix_encoding.fix_encoding()
[email protected]596cd5c2016-04-04 21:34:395028 setup_color.init()
[email protected]013731e2015-02-26 18:28:435029 try:
5030 sys.exit(main(sys.argv[1:]))
5031 except KeyboardInterrupt:
5032 sys.stderr.write('interrupted\n')
5033 sys.exit(1)