blob: 4750939598f3e502f97fce1e7729a83356f619e7 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000066DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000079# Shortcut since it quickly becomes redundant.
80Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000081
maruel@chromium.orgddd59412011-11-30 14:20:38 +000082# Initialized in main()
83settings = None
84
85
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000087 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 sys.exit(1)
89
90
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000091def GetNoGitPagerEnv():
92 env = os.environ.copy()
93 # 'cat' is a magical git string that disables pagers on all platforms.
94 env['GIT_PAGER'] = 'cat'
95 return env
96
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000097
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000098def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099 try:
maruel@chromium.org373af802012-05-25 21:07:33 +0000100 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000101 except subprocess2.CalledProcessError as e:
102 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000105 'Command "%s" failed.\n%s' % (
106 ' '.join(args), error_message or e.stdout or ''))
107 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108
109
110def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000111 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000112 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
114
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000116 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000117 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118 if suppress_stderr:
119 stderr = subprocess2.VOID
120 else:
121 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000122 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000123 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000124 stdout=subprocess2.PIPE,
125 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000126 return code, out[0]
127 except ValueError:
128 # When the subprocess fails, it returns None. That triggers a ValueError
129 # when trying to unpack the return value into (out, code).
130 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131
132
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000133def RunGitSilent(args):
134 """Returns stdout, suppresses stderr and ingores the return code."""
135 return RunGitWithCode(args, suppress_stderr=True)[1]
136
137
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 return (version.startswith(prefix) and
142 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000143
144
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000145def BranchExists(branch):
146 """Return True if specified branch exists."""
147 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
148 suppress_stderr=True)
149 return not code
150
151
maruel@chromium.org90541732011-04-01 17:54:18 +0000152def ask_for_data(prompt):
153 try:
154 return raw_input(prompt)
155 except KeyboardInterrupt:
156 # Hide the exception.
157 sys.exit(1)
158
159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160def git_set_branch_value(key, value):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000161 branch = GetCurrentBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000162 if not branch:
163 return
164
165 cmd = ['config']
166 if isinstance(value, int):
167 cmd.append('--int')
168 git_key = 'branch.%s.%s' % (branch, key)
169 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170
171
172def git_get_branch_default(key, default):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000173 branch = GetCurrentBranch()
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 if branch:
175 git_key = 'branch.%s.%s' % (branch, key)
176 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
177 try:
178 return int(stdout.strip())
179 except ValueError:
180 pass
181 return default
182
183
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184def add_git_similarity(parser):
185 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000187 help='Sets the percentage that a pair of files need to match in order to'
188 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000189 parser.add_option(
190 '--find-copies', action='store_true',
191 help='Allows git to look for copies.')
192 parser.add_option(
193 '--no-find-copies', action='store_false', dest='find_copies',
194 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
196 old_parser_args = parser.parse_args
197 def Parse(args):
198 options, args = old_parser_args(args)
199
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 print('Note: Saving similarity of %d%% in git config.'
204 % options.similarity)
205 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000206
iannucci@chromium.org79540052012-10-19 23:15:26 +0000207 options.similarity = max(0, min(options.similarity, 100))
208
209 if options.find_copies is None:
210 options.find_copies = bool(
211 git_get_branch_default('git-find-copies', True))
212 else:
213 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000214
215 print('Using %d%% similarity for rename/copy detection. '
216 'Override with --similarity.' % options.similarity)
217
218 return options, args
219 parser.parse_args = Parse
220
221
machenbach@chromium.org45453142015-09-15 08:45:22 +0000222def _get_properties_from_options(options):
223 properties = dict(x.split('=', 1) for x in options.properties)
224 for key, val in properties.iteritems():
225 try:
226 properties[key] = json.loads(val)
227 except ValueError:
228 pass # If a value couldn't be evaluated, treat it as a string.
229 return properties
230
231
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000232def _prefix_master(master):
233 """Convert user-specified master name to full master name.
234
235 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
236 name, while the developers always use shortened master name
237 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
238 function does the conversion for buildbucket migration.
239 """
240 prefix = 'master.'
241 if master.startswith(prefix):
242 return master
243 return '%s%s' % (prefix, master)
244
245
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000246def _buildbucket_retry(operation_name, http, *args, **kwargs):
247 """Retries requests to buildbucket service and returns parsed json content."""
248 try_count = 0
249 while True:
250 response, content = http.request(*args, **kwargs)
251 try:
252 content_json = json.loads(content)
253 except ValueError:
254 content_json = None
255
256 # Buildbucket could return an error even if status==200.
257 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000258 error = content_json.get('error')
259 if error.get('code') == 403:
260 raise BuildbucketResponseException(
261 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000262 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000263 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000264 raise BuildbucketResponseException(msg)
265
266 if response.status == 200:
267 if not content_json:
268 raise BuildbucketResponseException(
269 'Buildbucket returns invalid json content: %s.\n'
270 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
271 content)
272 return content_json
273 if response.status < 500 or try_count >= 2:
274 raise httplib2.HttpLib2Error(content)
275
276 # status >= 500 means transient failures.
277 logging.debug('Transient errors when %s. Will retry.', operation_name)
278 time.sleep(0.5 + 1.5*try_count)
279 try_count += 1
280 assert False, 'unreachable'
281
282
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000283def trigger_luci_job(changelist, masters, options):
284 """Send a job to run on LUCI."""
285 issue_props = changelist.GetIssueProperties()
286 issue = changelist.GetIssue()
287 patchset = changelist.GetMostRecentPatchset()
288 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000289 # TODO(hinoka et al): add support for other properties.
290 # Currently, this completely ignores testfilter and other properties.
291 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000292 luci_trigger.trigger(
293 builder, 'HEAD', issue, patchset, issue_props['project'])
294
295
machenbach@chromium.org45453142015-09-15 08:45:22 +0000296def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 rietveld_url = settings.GetDefaultServerUrl()
298 rietveld_host = urlparse.urlparse(rietveld_url).hostname
299 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
300 http = authenticator.authorize(httplib2.Http())
301 http.force_exception_to_status_code = True
302 issue_props = changelist.GetIssueProperties()
303 issue = changelist.GetIssue()
304 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000305 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000306
307 buildbucket_put_url = (
308 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000309 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000310 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
311 hostname=rietveld_host,
312 issue=issue,
313 patch=patchset)
314
315 batch_req_body = {'builds': []}
316 print_text = []
317 print_text.append('Tried jobs on:')
318 for master, builders_and_tests in sorted(masters.iteritems()):
319 print_text.append('Master: %s' % master)
320 bucket = _prefix_master(master)
321 for builder, tests in sorted(builders_and_tests.iteritems()):
322 print_text.append(' %s: %s' % (builder, tests))
323 parameters = {
324 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000325 'changes': [{
326 'author': {'email': issue_props['owner_email']},
327 'revision': options.revision,
328 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000329 'properties': {
330 'category': category,
331 'issue': issue,
332 'master': master,
333 'patch_project': issue_props['project'],
334 'patch_storage': 'rietveld',
335 'patchset': patchset,
336 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000337 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000338 },
339 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000340 if tests:
341 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000342 if properties:
343 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000344 if options.clobber:
345 parameters['properties']['clobber'] = True
346 batch_req_body['builds'].append(
347 {
348 'bucket': bucket,
349 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 'tags': ['builder:%s' % builder,
352 'buildset:%s' % buildset,
353 'master:%s' % master,
354 'user_agent:git_cl_try']
355 }
356 )
357
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 _buildbucket_retry(
359 'triggering tryjobs',
360 http,
361 buildbucket_put_url,
362 'PUT',
363 body=json.dumps(batch_req_body),
364 headers={'Content-Type': 'application/json'}
365 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000366 print_text.append('To see results here, run: git cl try-results')
367 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000368 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000369
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def fetch_try_jobs(auth_config, changelist, options):
372 """Fetches tryjobs from buildbucket.
373
374 Returns a map from build id to build info as json dictionary.
375 """
376 rietveld_url = settings.GetDefaultServerUrl()
377 rietveld_host = urlparse.urlparse(rietveld_url).hostname
378 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
379 if authenticator.has_cached_credentials():
380 http = authenticator.authorize(httplib2.Http())
381 else:
382 print ('Warning: Some results might be missing because %s' %
383 # Get the message on how to login.
384 auth.LoginRequiredError(rietveld_host).message)
385 http = httplib2.Http()
386
387 http.force_exception_to_status_code = True
388
389 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
390 hostname=rietveld_host,
391 issue=changelist.GetIssue(),
392 patch=options.patchset)
393 params = {'tag': 'buildset:%s' % buildset}
394
395 builds = {}
396 while True:
397 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
398 hostname=options.buildbucket_host,
399 params=urllib.urlencode(params))
400 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
401 for build in content.get('builds', []):
402 builds[build['id']] = build
403 if 'next_cursor' in content:
404 params['start_cursor'] = content['next_cursor']
405 else:
406 break
407 return builds
408
409
410def print_tryjobs(options, builds):
411 """Prints nicely result of fetch_try_jobs."""
412 if not builds:
413 print 'No tryjobs scheduled'
414 return
415
416 # Make a copy, because we'll be modifying builds dictionary.
417 builds = builds.copy()
418 builder_names_cache = {}
419
420 def get_builder(b):
421 try:
422 return builder_names_cache[b['id']]
423 except KeyError:
424 try:
425 parameters = json.loads(b['parameters_json'])
426 name = parameters['builder_name']
427 except (ValueError, KeyError) as error:
428 print 'WARNING: failed to get builder name for build %s: %s' % (
429 b['id'], error)
430 name = None
431 builder_names_cache[b['id']] = name
432 return name
433
434 def get_bucket(b):
435 bucket = b['bucket']
436 if bucket.startswith('master.'):
437 return bucket[len('master.'):]
438 return bucket
439
440 if options.print_master:
441 name_fmt = '%%-%ds %%-%ds' % (
442 max(len(str(get_bucket(b))) for b in builds.itervalues()),
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % (get_bucket(b), get_builder(b))
446 else:
447 name_fmt = '%%-%ds' % (
448 max(len(str(get_builder(b))) for b in builds.itervalues()))
449 def get_name(b):
450 return name_fmt % get_builder(b)
451
452 def sort_key(b):
453 return b['status'], b.get('result'), get_name(b), b.get('url')
454
455 def pop(title, f, color=None, **kwargs):
456 """Pop matching builds from `builds` dict and print them."""
457
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000458 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459 colorize = str
460 else:
461 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
462
463 result = []
464 for b in builds.values():
465 if all(b.get(k) == v for k, v in kwargs.iteritems()):
466 builds.pop(b['id'])
467 result.append(b)
468 if result:
469 print colorize(title)
470 for b in sorted(result, key=sort_key):
471 print ' ', colorize('\t'.join(map(str, f(b))))
472
473 total = len(builds)
474 pop(status='COMPLETED', result='SUCCESS',
475 title='Successes:', color=Fore.GREEN,
476 f=lambda b: (get_name(b), b.get('url')))
477 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
478 title='Infra Failures:', color=Fore.MAGENTA,
479 f=lambda b: (get_name(b), b.get('url')))
480 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
481 title='Failures:', color=Fore.RED,
482 f=lambda b: (get_name(b), b.get('url')))
483 pop(status='COMPLETED', result='CANCELED',
484 title='Canceled:', color=Fore.MAGENTA,
485 f=lambda b: (get_name(b),))
486 pop(status='COMPLETED', result='FAILURE',
487 failure_reason='INVALID_BUILD_DEFINITION',
488 title='Wrong master/builder name:', color=Fore.MAGENTA,
489 f=lambda b: (get_name(b),))
490 pop(status='COMPLETED', result='FAILURE',
491 title='Other failures:',
492 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
493 pop(status='COMPLETED',
494 title='Other finished:',
495 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
496 pop(status='STARTED',
497 title='Started:', color=Fore.YELLOW,
498 f=lambda b: (get_name(b), b.get('url')))
499 pop(status='SCHEDULED',
500 title='Scheduled:',
501 f=lambda b: (get_name(b), 'id=%s' % b['id']))
502 # The last section is just in case buildbucket API changes OR there is a bug.
503 pop(title='Other:',
504 f=lambda b: (get_name(b), 'id=%s' % b['id']))
505 assert len(builds) == 0
506 print 'Total: %d tryjobs' % total
507
508
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000509def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
510 """Return the corresponding git ref if |base_url| together with |glob_spec|
511 matches the full |url|.
512
513 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
514 """
515 fetch_suburl, as_ref = glob_spec.split(':')
516 if allow_wildcards:
517 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
518 if glob_match:
519 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
520 # "branches/{472,597,648}/src:refs/remotes/svn/*".
521 branch_re = re.escape(base_url)
522 if glob_match.group(1):
523 branch_re += '/' + re.escape(glob_match.group(1))
524 wildcard = glob_match.group(2)
525 if wildcard == '*':
526 branch_re += '([^/]*)'
527 else:
528 # Escape and replace surrounding braces with parentheses and commas
529 # with pipe symbols.
530 wildcard = re.escape(wildcard)
531 wildcard = re.sub('^\\\\{', '(', wildcard)
532 wildcard = re.sub('\\\\,', '|', wildcard)
533 wildcard = re.sub('\\\\}$', ')', wildcard)
534 branch_re += wildcard
535 if glob_match.group(3):
536 branch_re += re.escape(glob_match.group(3))
537 match = re.match(branch_re, url)
538 if match:
539 return re.sub('\*$', match.group(1), as_ref)
540
541 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
542 if fetch_suburl:
543 full_url = base_url + '/' + fetch_suburl
544 else:
545 full_url = base_url
546 if full_url == url:
547 return as_ref
548 return None
549
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000550
iannucci@chromium.org79540052012-10-19 23:15:26 +0000551def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 """Prints statistics about the change to the user."""
553 # --no-ext-diff is broken in some versions of Git, so try to work around
554 # this by overriding the environment (but there is still a problem if the
555 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000556 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000557 if 'GIT_EXTERNAL_DIFF' in env:
558 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000559
560 if find_copies:
561 similarity_options = ['--find-copies-harder', '-l100000',
562 '-C%s' % similarity]
563 else:
564 similarity_options = ['-M%s' % similarity]
565
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 try:
567 stdout = sys.stdout.fileno()
568 except AttributeError:
569 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000570 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000571 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000572 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000573 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000574
575
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000576class BuildbucketResponseException(Exception):
577 pass
578
579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580class Settings(object):
581 def __init__(self):
582 self.default_server = None
583 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000584 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 self.is_git_svn = None
586 self.svn_branch = None
587 self.tree_status_url = None
588 self.viewvc_url = None
589 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000590 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000591 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000592 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000594 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000595 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596
597 def LazyUpdateIfNeeded(self):
598 """Updates the settings from a codereview.settings file, if available."""
599 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 # The only value that actually changes the behavior is
601 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000602 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000603 error_ok=True
604 ).strip().lower()
605
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000607 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 LoadCodereviewSettingsFromFile(cr_settings_file)
609 self.updated = True
610
611 def GetDefaultServerUrl(self, error_ok=False):
612 if not self.default_server:
613 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000615 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 if error_ok:
617 return self.default_server
618 if not self.default_server:
619 error_message = ('Could not find settings file. You must configure '
620 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000621 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000622 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return self.default_server
624
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000625 @staticmethod
626 def GetRelativeRoot():
627 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000630 if self.root is None:
631 self.root = os.path.abspath(self.GetRelativeRoot())
632 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000634 def GetGitMirror(self, remote='origin'):
635 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000636 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000637 if not os.path.isdir(local_url):
638 return None
639 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
640 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
641 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
642 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
643 if mirror.exists():
644 return mirror
645 return None
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def GetIsGitSvn(self):
648 """Return true if this repo looks like it's using git-svn."""
649 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000650 if self.GetPendingRefPrefix():
651 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
652 self.is_git_svn = False
653 else:
654 # If you have any "svn-remote.*" config keys, we think you're using svn.
655 self.is_git_svn = RunGitWithCode(
656 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000657 return self.is_git_svn
658
659 def GetSVNBranch(self):
660 if self.svn_branch is None:
661 if not self.GetIsGitSvn():
662 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
663
664 # Try to figure out which remote branch we're based on.
665 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000666 # 1) iterate through our branch history and find the svn URL.
667 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668
669 # regexp matching the git-svn line that contains the URL.
670 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 # We don't want to go through all of history, so read a line from the
673 # pipe at a time.
674 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000675 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000676 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
677 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000678 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 for line in proc.stdout:
680 match = git_svn_re.match(line)
681 if match:
682 url = match.group(1)
683 proc.stdout.close() # Cut pipe.
684 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 if url:
687 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
688 remotes = RunGit(['config', '--get-regexp',
689 r'^svn-remote\..*\.url']).splitlines()
690 for remote in remotes:
691 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 remote = match.group(1)
694 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000695 rewrite_root = RunGit(
696 ['config', 'svn-remote.%s.rewriteRoot' % remote],
697 error_ok=True).strip()
698 if rewrite_root:
699 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000701 ['config', 'svn-remote.%s.fetch' % remote],
702 error_ok=True).strip()
703 if fetch_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
705 if self.svn_branch:
706 break
707 branch_spec = RunGit(
708 ['config', 'svn-remote.%s.branches' % remote],
709 error_ok=True).strip()
710 if branch_spec:
711 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
712 if self.svn_branch:
713 break
714 tag_spec = RunGit(
715 ['config', 'svn-remote.%s.tags' % remote],
716 error_ok=True).strip()
717 if tag_spec:
718 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
719 if self.svn_branch:
720 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721
722 if not self.svn_branch:
723 DieWithError('Can\'t guess svn branch -- try specifying it on the '
724 'command line')
725
726 return self.svn_branch
727
728 def GetTreeStatusUrl(self, error_ok=False):
729 if not self.tree_status_url:
730 error_message = ('You must configure your tree status URL by running '
731 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000732 self.tree_status_url = self._GetRietveldConfig(
733 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 return self.tree_status_url
735
736 def GetViewVCUrl(self):
737 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 return self.viewvc_url
740
rmistry@google.com90752582014-01-14 21:04:50 +0000741 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000743
rmistry@google.com78948ed2015-07-08 23:09:57 +0000744 def GetIsSkipDependencyUpload(self, branch_name):
745 """Returns true if specified branch should skip dep uploads."""
746 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
747 error_ok=True)
748
rmistry@google.com5626a922015-02-26 14:03:30 +0000749 def GetRunPostUploadHook(self):
750 run_post_upload_hook = self._GetRietveldConfig(
751 'run-post-upload-hook', error_ok=True)
752 return run_post_upload_hook == "True"
753
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000754 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000755 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000756
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000757 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000759
ukai@chromium.orge8077812012-02-03 03:41:46 +0000760 def GetIsGerrit(self):
761 """Return true if this repo is assosiated with gerrit code review system."""
762 if self.is_gerrit is None:
763 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
764 return self.is_gerrit
765
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000766 def GetSquashGerritUploads(self):
767 """Return true if uploads to Gerrit should be squashed by default."""
768 if self.squash_gerrit_uploads is None:
769 self.squash_gerrit_uploads = (
770 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
771 error_ok=True).strip() == 'true')
772 return self.squash_gerrit_uploads
773
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000774 def GetGitEditor(self):
775 """Return the editor specified in the git config, or None if none is."""
776 if self.git_editor is None:
777 self.git_editor = self._GetConfig('core.editor', error_ok=True)
778 return self.git_editor or None
779
thestig@chromium.org44202a22014-03-11 19:22:18 +0000780 def GetLintRegex(self):
781 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
782 DEFAULT_LINT_REGEX)
783
784 def GetLintIgnoreRegex(self):
785 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
786 DEFAULT_LINT_IGNORE_REGEX)
787
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000788 def GetProject(self):
789 if not self.project:
790 self.project = self._GetRietveldConfig('project', error_ok=True)
791 return self.project
792
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000793 def GetForceHttpsCommitUrl(self):
794 if not self.force_https_commit_url:
795 self.force_https_commit_url = self._GetRietveldConfig(
796 'force-https-commit-url', error_ok=True)
797 return self.force_https_commit_url
798
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000799 def GetPendingRefPrefix(self):
800 if not self.pending_ref_prefix:
801 self.pending_ref_prefix = self._GetRietveldConfig(
802 'pending-ref-prefix', error_ok=True)
803 return self.pending_ref_prefix
804
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 def _GetRietveldConfig(self, param, **kwargs):
806 return self._GetConfig('rietveld.' + param, **kwargs)
807
rmistry@google.com78948ed2015-07-08 23:09:57 +0000808 def _GetBranchConfig(self, branch_name, param, **kwargs):
809 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000811 def _GetConfig(self, param, **kwargs):
812 self.LazyUpdateIfNeeded()
813 return RunGit(['config', param], **kwargs).strip()
814
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816def ShortBranchName(branch):
817 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000818 return branch.replace('refs/heads/', '', 1)
819
820
821def GetCurrentBranchRef():
822 """Returns branch ref (e.g., refs/heads/master) or None."""
823 return RunGit(['symbolic-ref', 'HEAD'],
824 stderr=subprocess2.VOID, error_ok=True).strip() or None
825
826
827def GetCurrentBranch():
828 """Returns current branch or None.
829
830 For refs/heads/* branches, returns just last part. For others, full ref.
831 """
832 branchref = GetCurrentBranchRef()
833 if branchref:
834 return ShortBranchName(branchref)
835 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000838class _ParsedIssueNumberArgument(object):
839 def __init__(self, issue=None, patchset=None, hostname=None):
840 self.issue = issue
841 self.patchset = patchset
842 self.hostname = hostname
843
844 @property
845 def valid(self):
846 return self.issue is not None
847
848
849class _RietveldParsedIssueNumberArgument(_ParsedIssueNumberArgument):
850 def __init__(self, *args, **kwargs):
851 self.patch_url = kwargs.pop('patch_url', None)
852 super(_RietveldParsedIssueNumberArgument, self).__init__(*args, **kwargs)
853
854
855def ParseIssueNumberArgument(arg):
856 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
857 fail_result = _ParsedIssueNumberArgument()
858
859 if arg.isdigit():
860 return _ParsedIssueNumberArgument(issue=int(arg))
861 if not arg.startswith('http'):
862 return fail_result
863 url = gclient_utils.UpgradeToHttps(arg)
864 try:
865 parsed_url = urlparse.urlparse(url)
866 except ValueError:
867 return fail_result
868 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
869 tmp = cls.ParseIssueURL(parsed_url)
870 if tmp is not None:
871 return tmp
872 return fail_result
873
874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000876 """Changelist works with one changelist in local branch.
877
878 Supports two codereview backends: Rietveld or Gerrit, selected at object
879 creation.
880
881 Not safe for concurrent multi-{thread,process} use.
882 """
883
884 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
885 """Create a new ChangeList instance.
886
887 If issue is given, the codereview must be given too.
888
889 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
890 Otherwise, it's decided based on current configuration of the local branch,
891 with default being 'rietveld' for backwards compatibility.
892 See _load_codereview_impl for more details.
893
894 **kwargs will be passed directly to codereview implementation.
895 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000897 global settings
898 if not settings:
899 # Happens when git_cl.py is used as a utility library.
900 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000901
902 if issue:
903 assert codereview, 'codereview must be known, if issue is known'
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.branchref = branchref
906 if self.branchref:
907 self.branch = ShortBranchName(self.branchref)
908 else:
909 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000911 self.lookedup_issue = False
912 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 self.has_description = False
914 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000915 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000917 self.cc = None
918 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000919 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000920
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000921 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000922 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000923 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000924 assert self._codereview_impl
925 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000926
927 def _load_codereview_impl(self, codereview=None, **kwargs):
928 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000929 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
930 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
931 self._codereview = codereview
932 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000933 return
934
935 # Automatic selection based on issue number set for a current branch.
936 # Rietveld takes precedence over Gerrit.
937 assert not self.issue
938 # Whether we find issue or not, we are doing the lookup.
939 self.lookedup_issue = True
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000940 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000941 setting = cls.IssueSetting(self.GetBranch())
942 issue = RunGit(['config', setting], error_ok=True).strip()
943 if issue:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000944 self._codereview = codereview
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000945 self._codereview_impl = cls(self, **kwargs)
946 self.issue = int(issue)
947 return
948
949 # No issue is set for this branch, so decide based on repo-wide settings.
950 return self._load_codereview_impl(
951 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
952 **kwargs)
953
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +0000954 def IsGerrit(self):
955 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000956
957 def GetCCList(self):
958 """Return the users cc'd on this CL.
959
960 Return is a string suitable for passing to gcl with the --cc flag.
961 """
962 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000963 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000964 more_cc = ','.join(self.watchers)
965 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
966 return self.cc
967
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000968 def GetCCListWithoutDefault(self):
969 """Return the users cc'd on this CL excluding default ones."""
970 if self.cc is None:
971 self.cc = ','.join(self.watchers)
972 return self.cc
973
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000974 def SetWatchers(self, watchers):
975 """Set the list of email addresses that should be cc'd based on the changed
976 files in this CL.
977 """
978 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979
980 def GetBranch(self):
981 """Returns the short branch name, e.g. 'master'."""
982 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000984 if not branchref:
985 return None
986 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 self.branch = ShortBranchName(self.branchref)
988 return self.branch
989
990 def GetBranchRef(self):
991 """Returns the full branch name, e.g. 'refs/heads/master'."""
992 self.GetBranch() # Poke the lazy loader.
993 return self.branchref
994
tandrii@chromium.org534f67a2016-04-07 18:47:05 +0000995 def ClearBranch(self):
996 """Clears cached branch data of this object."""
997 self.branch = self.branchref = None
998
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000999 @staticmethod
1000 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001001 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 e.g. 'origin', 'refs/heads/master'
1003 """
1004 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
1006 error_ok=True).strip()
1007 if upstream_branch:
1008 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
1009 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001010 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1011 error_ok=True).strip()
1012 if upstream_branch:
1013 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001015 # Fall back on trying a git-svn upstream branch.
1016 if settings.GetIsGitSvn():
1017 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001019 # Else, try to guess the origin remote.
1020 remote_branches = RunGit(['branch', '-r']).split()
1021 if 'origin/master' in remote_branches:
1022 # Fall back on origin/master if it exits.
1023 remote = 'origin'
1024 upstream_branch = 'refs/heads/master'
1025 elif 'origin/trunk' in remote_branches:
1026 # Fall back on origin/trunk if it exists. Generally a shared
1027 # git-svn clone
1028 remote = 'origin'
1029 upstream_branch = 'refs/heads/trunk'
1030 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001031 DieWithError(
1032 'Unable to determine default branch to diff against.\n'
1033 'Either pass complete "git diff"-style arguments, like\n'
1034 ' git cl upload origin/master\n'
1035 'or verify this branch is set up to track another \n'
1036 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037
1038 return remote, upstream_branch
1039
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001040 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001041 upstream_branch = self.GetUpstreamBranch()
1042 if not BranchExists(upstream_branch):
1043 DieWithError('The upstream for the current branch (%s) does not exist '
1044 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001045 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001046 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001048 def GetUpstreamBranch(self):
1049 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001050 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001052 upstream_branch = upstream_branch.replace('refs/heads/',
1053 'refs/remotes/%s/' % remote)
1054 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1055 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 self.upstream_branch = upstream_branch
1057 return self.upstream_branch
1058
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001059 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001060 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001061 remote, branch = None, self.GetBranch()
1062 seen_branches = set()
1063 while branch not in seen_branches:
1064 seen_branches.add(branch)
1065 remote, branch = self.FetchUpstreamTuple(branch)
1066 branch = ShortBranchName(branch)
1067 if remote != '.' or branch.startswith('refs/remotes'):
1068 break
1069 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001070 remotes = RunGit(['remote'], error_ok=True).split()
1071 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001072 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001073 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001074 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001075 logging.warning('Could not determine which remote this change is '
1076 'associated with, so defaulting to "%s". This may '
1077 'not be what you want. You may prevent this message '
1078 'by running "git svn info" as documented here: %s',
1079 self._remote,
1080 GIT_INSTRUCTIONS_URL)
1081 else:
1082 logging.warn('Could not determine which remote this change is '
1083 'associated with. You may prevent this message by '
1084 'running "git svn info" as documented here: %s',
1085 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001086 branch = 'HEAD'
1087 if branch.startswith('refs/remotes'):
1088 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001089 elif branch.startswith('refs/branch-heads/'):
1090 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001091 else:
1092 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001093 return self._remote
1094
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001095 def GitSanityChecks(self, upstream_git_obj):
1096 """Checks git repo status and ensures diff is from local commits."""
1097
sbc@chromium.org79706062015-01-14 21:18:12 +00001098 if upstream_git_obj is None:
1099 if self.GetBranch() is None:
1100 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001101 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +00001102 else:
1103 print >> sys.stderr, (
1104 'ERROR: no upstream branch')
1105 return False
1106
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001107 # Verify the commit we're diffing against is in our current branch.
1108 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1109 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1110 if upstream_sha != common_ancestor:
1111 print >> sys.stderr, (
1112 'ERROR: %s is not in the current branch. You may need to rebase '
1113 'your tracking branch' % upstream_sha)
1114 return False
1115
1116 # List the commits inside the diff, and verify they are all local.
1117 commits_in_diff = RunGit(
1118 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1119 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1120 remote_branch = remote_branch.strip()
1121 if code != 0:
1122 _, remote_branch = self.GetRemoteBranch()
1123
1124 commits_in_remote = RunGit(
1125 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1126
1127 common_commits = set(commits_in_diff) & set(commits_in_remote)
1128 if common_commits:
1129 print >> sys.stderr, (
1130 'ERROR: Your diff contains %d commits already in %s.\n'
1131 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1132 'the diff. If you are using a custom git flow, you can override'
1133 ' the reference used for this check with "git config '
1134 'gitcl.remotebranch <git-ref>".' % (
1135 len(common_commits), remote_branch, upstream_git_obj))
1136 return False
1137 return True
1138
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001139 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001140 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001141
1142 Returns None if it is not set.
1143 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001144 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1145 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001146
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001147 def GetGitSvnRemoteUrl(self):
1148 """Return the configured git-svn remote URL parsed from git svn info.
1149
1150 Returns None if it is not set.
1151 """
1152 # URL is dependent on the current directory.
1153 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1154 if data:
1155 keys = dict(line.split(': ', 1) for line in data.splitlines()
1156 if ': ' in line)
1157 return keys.get('URL', None)
1158 return None
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 def GetRemoteUrl(self):
1161 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1162
1163 Returns None if there is no remote.
1164 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001165 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001166 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1167
1168 # If URL is pointing to a local directory, it is probably a git cache.
1169 if os.path.isdir(url):
1170 url = RunGit(['config', 'remote.%s.url' % remote],
1171 error_ok=True,
1172 cwd=url).strip()
1173 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001175 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001176 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001177 if self.issue is None and not self.lookedup_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 issue = RunGit(['config',
1179 self._codereview_impl.IssueSetting(self.GetBranch())],
1180 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001181 self.issue = int(issue) or None if issue else None
1182 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183 return self.issue
1184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 def GetIssueURL(self):
1186 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001187 issue = self.GetIssue()
1188 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001189 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001190 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
1192 def GetDescription(self, pretty=False):
1193 if not self.has_description:
1194 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001195 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 self.has_description = True
1197 if pretty:
1198 wrapper = textwrap.TextWrapper()
1199 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1200 return wrapper.fill(self.description)
1201 return self.description
1202
1203 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001204 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001205 if self.patchset is None and not self.lookedup_patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001206 patchset = RunGit(['config', self._codereview_impl.PatchsetSetting()],
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001208 self.patchset = int(patchset) or None if patchset else None
1209 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 return self.patchset
1211
1212 def SetPatchset(self, patchset):
1213 """Set this branch's patchset. If patchset=0, clears the patchset."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 patchset_setting = self._codereview_impl.PatchsetSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if patchset:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001216 RunGit(['config', patchset_setting, str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001217 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001219 RunGit(['config', '--unset', patchset_setting],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001220 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001221 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001223 def SetIssue(self, issue=None):
1224 """Set this branch's issue. If issue isn't given, clears the issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001225 issue_setting = self._codereview_impl.IssueSetting(self.GetBranch())
1226 codereview_setting = self._codereview_impl.GetCodereviewServerSetting()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001228 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001229 RunGit(['config', issue_setting, str(issue)])
1230 codereview_server = self._codereview_impl.GetCodereviewServer()
1231 if codereview_server:
1232 RunGit(['config', codereview_setting, codereview_server])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001234 current_issue = self.GetIssue()
1235 if current_issue:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001236 RunGit(['config', '--unset', issue_setting])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001237 self.issue = None
1238 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001240 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001241 if not self.GitSanityChecks(upstream_branch):
1242 DieWithError('\nGit sanity check failure')
1243
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001244 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001245 if not root:
1246 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001247 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001248
1249 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001250 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001251 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001252 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001253 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001254 except subprocess2.CalledProcessError:
1255 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001256 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001257 'This branch probably doesn\'t exist anymore. To reset the\n'
1258 'tracking branch, please run\n'
1259 ' git branch --set-upstream %s trunk\n'
1260 'replacing trunk with origin/master or the relevant branch') %
1261 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001262
maruel@chromium.org52424302012-08-29 15:14:30 +00001263 issue = self.GetIssue()
1264 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001265 if issue:
1266 description = self.GetDescription()
1267 else:
1268 # If the change was never uploaded, use the log messages of all commits
1269 # up to the branch point, as git cl upload will prefill the description
1270 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001271 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1272 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001273
1274 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001275 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001276 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001277 name,
1278 description,
1279 absroot,
1280 files,
1281 issue,
1282 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001283 author,
1284 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001285
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001286 def UpdateDescription(self, description):
1287 self.description = description
1288 return self._codereview_impl.UpdateDescriptionRemote(description)
1289
1290 def RunHook(self, committing, may_prompt, verbose, change):
1291 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1292 try:
1293 return presubmit_support.DoPresubmitChecks(change, committing,
1294 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1295 default_presubmit=None, may_prompt=may_prompt,
1296 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit())
1297 except presubmit_support.PresubmitFailure, e:
1298 DieWithError(
1299 ('%s\nMaybe your depot_tools is out of date?\n'
1300 'If all fails, contact maruel@') % e)
1301
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001302 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1303 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001304 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1305 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001306 else:
1307 # Assume url.
1308 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1309 urlparse.urlparse(issue_arg))
1310 if not parsed_issue_arg or not parsed_issue_arg.valid:
1311 DieWithError('Failed to parse issue argument "%s". '
1312 'Must be an issue number or a valid URL.' % issue_arg)
1313 return self._codereview_impl.CMDPatchWithParsedIssue(
1314 parsed_issue_arg, reject, nocommit, directory)
1315
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001316 def CMDUpload(self, options, git_diff_args, orig_args):
1317 """Uploads a change to codereview."""
1318 if git_diff_args:
1319 # TODO(ukai): is it ok for gerrit case?
1320 base_branch = git_diff_args[0]
1321 else:
1322 if self.GetBranch() is None:
1323 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1324
1325 # Default to diffing against common ancestor of upstream branch
1326 base_branch = self.GetCommonAncestorWithUpstream()
1327 git_diff_args = [base_branch, 'HEAD']
1328
1329 # Make sure authenticated to codereview before running potentially expensive
1330 # hooks. It is a fast, best efforts check. Codereview still can reject the
1331 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001332 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001333
1334 # Apply watchlists on upload.
1335 change = self.GetChange(base_branch, None)
1336 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1337 files = [f.LocalPath() for f in change.AffectedFiles()]
1338 if not options.bypass_watchlists:
1339 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1340
1341 if not options.bypass_hooks:
1342 if options.reviewers or options.tbr_owners:
1343 # Set the reviewer list now so that presubmit checks can access it.
1344 change_description = ChangeDescription(change.FullDescriptionText())
1345 change_description.update_reviewers(options.reviewers,
1346 options.tbr_owners,
1347 change)
1348 change.SetDescriptionText(change_description.description)
1349 hook_results = self.RunHook(committing=False,
1350 may_prompt=not options.force,
1351 verbose=options.verbose,
1352 change=change)
1353 if not hook_results.should_continue():
1354 return 1
1355 if not options.reviewers and hook_results.reviewers:
1356 options.reviewers = hook_results.reviewers.split(',')
1357
1358 if self.GetIssue():
1359 latest_patchset = self.GetMostRecentPatchset()
1360 local_patchset = self.GetPatchset()
1361 if (latest_patchset and local_patchset and
1362 local_patchset != latest_patchset):
1363 print ('The last upload made from this repository was patchset #%d but '
1364 'the most recent patchset on the server is #%d.'
1365 % (local_patchset, latest_patchset))
1366 print ('Uploading will still work, but if you\'ve uploaded to this '
1367 'issue from another machine or branch the patch you\'re '
1368 'uploading now might not include those changes.')
1369 ask_for_data('About to upload; enter to confirm.')
1370
1371 print_stats(options.similarity, options.find_copies, git_diff_args)
1372 ret = self.CMDUploadChange(options, git_diff_args, change)
1373 if not ret:
1374 git_set_branch_value('last-upload-hash',
1375 RunGit(['rev-parse', 'HEAD']).strip())
1376 # Run post upload hooks, if specified.
1377 if settings.GetRunPostUploadHook():
1378 presubmit_support.DoPostUploadExecuter(
1379 change,
1380 self,
1381 settings.GetRoot(),
1382 options.verbose,
1383 sys.stdout)
1384
1385 # Upload all dependencies if specified.
1386 if options.dependencies:
1387 print
1388 print '--dependencies has been specified.'
1389 print 'All dependent local branches will be re-uploaded.'
1390 print
1391 # Remove the dependencies flag from args so that we do not end up in a
1392 # loop.
1393 orig_args.remove('--dependencies')
1394 ret = upload_branch_deps(self, orig_args)
1395 return ret
1396
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001397 # Forward methods to codereview specific implementation.
1398
1399 def CloseIssue(self):
1400 return self._codereview_impl.CloseIssue()
1401
1402 def GetStatus(self):
1403 return self._codereview_impl.GetStatus()
1404
1405 def GetCodereviewServer(self):
1406 return self._codereview_impl.GetCodereviewServer()
1407
1408 def GetApprovingReviewers(self):
1409 return self._codereview_impl.GetApprovingReviewers()
1410
1411 def GetMostRecentPatchset(self):
1412 return self._codereview_impl.GetMostRecentPatchset()
1413
1414 def __getattr__(self, attr):
1415 # This is because lots of untested code accesses Rietveld-specific stuff
1416 # directly, and it's hard to fix for sure. So, just let it work, and fix
1417 # on a cases by case basis.
1418 return getattr(self._codereview_impl, attr)
1419
1420
1421class _ChangelistCodereviewBase(object):
1422 """Abstract base class encapsulating codereview specifics of a changelist."""
1423 def __init__(self, changelist):
1424 self._changelist = changelist # instance of Changelist
1425
1426 def __getattr__(self, attr):
1427 # Forward methods to changelist.
1428 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1429 # _RietveldChangelistImpl to avoid this hack?
1430 return getattr(self._changelist, attr)
1431
1432 def GetStatus(self):
1433 """Apply a rough heuristic to give a simple summary of an issue's review
1434 or CQ status, assuming adherence to a common workflow.
1435
1436 Returns None if no issue for this branch, or specific string keywords.
1437 """
1438 raise NotImplementedError()
1439
1440 def GetCodereviewServer(self):
1441 """Returns server URL without end slash, like "https://codereview.com"."""
1442 raise NotImplementedError()
1443
1444 def FetchDescription(self):
1445 """Fetches and returns description from the codereview server."""
1446 raise NotImplementedError()
1447
1448 def GetCodereviewServerSetting(self):
1449 """Returns git config setting for the codereview server."""
1450 raise NotImplementedError()
1451
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001452 @classmethod
1453 def IssueSetting(cls, branch):
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001454 return 'branch.%s.%s' % (branch, cls.IssueSettingSuffix())
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001455
1456 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001457 def IssueSettingSuffix(cls):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001458 """Returns name of git config setting which stores issue number for a given
1459 branch."""
1460 raise NotImplementedError()
1461
1462 def PatchsetSetting(self):
1463 """Returns name of git config setting which stores issue number."""
1464 raise NotImplementedError()
1465
1466 def GetRieveldObjForPresubmit(self):
1467 # This is an unfortunate Rietveld-embeddedness in presubmit.
1468 # For non-Rietveld codereviews, this probably should return a dummy object.
1469 raise NotImplementedError()
1470
1471 def UpdateDescriptionRemote(self, description):
1472 """Update the description on codereview site."""
1473 raise NotImplementedError()
1474
1475 def CloseIssue(self):
1476 """Closes the issue."""
1477 raise NotImplementedError()
1478
1479 def GetApprovingReviewers(self):
1480 """Returns a list of reviewers approving the change.
1481
1482 Note: not necessarily committers.
1483 """
1484 raise NotImplementedError()
1485
1486 def GetMostRecentPatchset(self):
1487 """Returns the most recent patchset number from the codereview site."""
1488 raise NotImplementedError()
1489
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001490 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1491 directory):
1492 """Fetches and applies the issue.
1493
1494 Arguments:
1495 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1496 reject: if True, reject the failed patch instead of switching to 3-way
1497 merge. Rietveld only.
1498 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1499 only.
1500 directory: switch to directory before applying the patch. Rietveld only.
1501 """
1502 raise NotImplementedError()
1503
1504 @staticmethod
1505 def ParseIssueURL(parsed_url):
1506 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1507 failed."""
1508 raise NotImplementedError()
1509
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001510 def EnsureAuthenticated(self, force):
1511 """Best effort check that user is authenticated with codereview server.
1512
1513 Arguments:
1514 force: whether to skip confirmation questions.
1515 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001516 raise NotImplementedError()
1517
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001518 def CMDUploadChange(self, options, args, change):
1519 """Uploads a change to codereview."""
1520 raise NotImplementedError()
1521
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001522
1523class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1524 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1525 super(_RietveldChangelistImpl, self).__init__(changelist)
1526 assert settings, 'must be initialized in _ChangelistCodereviewBase'
1527 settings.GetDefaultServerUrl()
1528
1529 self._rietveld_server = rietveld_server
1530 self._auth_config = auth_config
1531 self._props = None
1532 self._rpc_server = None
1533
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001534 def GetCodereviewServer(self):
1535 if not self._rietveld_server:
1536 # If we're on a branch then get the server potentially associated
1537 # with that branch.
1538 if self.GetIssue():
1539 rietveld_server_setting = self.GetCodereviewServerSetting()
1540 if rietveld_server_setting:
1541 self._rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1542 ['config', rietveld_server_setting], error_ok=True).strip())
1543 if not self._rietveld_server:
1544 self._rietveld_server = settings.GetDefaultServerUrl()
1545 return self._rietveld_server
1546
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001547 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 """Best effort check that user is authenticated with Rietveld server."""
1549 if self._auth_config.use_oauth2:
1550 authenticator = auth.get_authenticator_for_host(
1551 self.GetCodereviewServer(), self._auth_config)
1552 if not authenticator.has_cached_credentials():
1553 raise auth.LoginRequiredError(self.GetCodereviewServer())
1554
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001555 def FetchDescription(self):
1556 issue = self.GetIssue()
1557 assert issue
1558 try:
1559 return self.RpcServer().get_description(issue).strip()
1560 except urllib2.HTTPError as e:
1561 if e.code == 404:
1562 DieWithError(
1563 ('\nWhile fetching the description for issue %d, received a '
1564 '404 (not found)\n'
1565 'error. It is likely that you deleted this '
1566 'issue on the server. If this is the\n'
1567 'case, please run\n\n'
1568 ' git cl issue 0\n\n'
1569 'to clear the association with the deleted issue. Then run '
1570 'this command again.') % issue)
1571 else:
1572 DieWithError(
1573 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1574 except urllib2.URLError as e:
1575 print >> sys.stderr, (
1576 'Warning: Failed to retrieve CL description due to network '
1577 'failure.')
1578 return ''
1579
1580 def GetMostRecentPatchset(self):
1581 return self.GetIssueProperties()['patchsets'][-1]
1582
1583 def GetPatchSetDiff(self, issue, patchset):
1584 return self.RpcServer().get(
1585 '/download/issue%s_%s.diff' % (issue, patchset))
1586
1587 def GetIssueProperties(self):
1588 if self._props is None:
1589 issue = self.GetIssue()
1590 if not issue:
1591 self._props = {}
1592 else:
1593 self._props = self.RpcServer().get_issue_properties(issue, True)
1594 return self._props
1595
1596 def GetApprovingReviewers(self):
1597 return get_approving_reviewers(self.GetIssueProperties())
1598
1599 def AddComment(self, message):
1600 return self.RpcServer().add_comment(self.GetIssue(), message)
1601
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001602 def GetStatus(self):
1603 """Apply a rough heuristic to give a simple summary of an issue's review
1604 or CQ status, assuming adherence to a common workflow.
1605
1606 Returns None if no issue for this branch, or one of the following keywords:
1607 * 'error' - error from review tool (including deleted issues)
1608 * 'unsent' - not sent for review
1609 * 'waiting' - waiting for review
1610 * 'reply' - waiting for owner to reply to review
1611 * 'lgtm' - LGTM from at least one approved reviewer
1612 * 'commit' - in the commit queue
1613 * 'closed' - closed
1614 """
1615 if not self.GetIssue():
1616 return None
1617
1618 try:
1619 props = self.GetIssueProperties()
1620 except urllib2.HTTPError:
1621 return 'error'
1622
1623 if props.get('closed'):
1624 # Issue is closed.
1625 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001626 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001627 # Issue is in the commit queue.
1628 return 'commit'
1629
1630 try:
1631 reviewers = self.GetApprovingReviewers()
1632 except urllib2.HTTPError:
1633 return 'error'
1634
1635 if reviewers:
1636 # Was LGTM'ed.
1637 return 'lgtm'
1638
1639 messages = props.get('messages') or []
1640
1641 if not messages:
1642 # No message was sent.
1643 return 'unsent'
1644 if messages[-1]['sender'] != props.get('owner_email'):
1645 # Non-LGTM reply from non-owner
1646 return 'reply'
1647 return 'waiting'
1648
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001649 def UpdateDescriptionRemote(self, description):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001650 return self.RpcServer().update_description(
1651 self.GetIssue(), self.description)
1652
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001654 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001656 def SetFlag(self, flag, value):
1657 """Patchset must match."""
1658 if not self.GetPatchset():
1659 DieWithError('The patchset needs to match. Send another patchset.')
1660 try:
1661 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001662 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001663 except urllib2.HTTPError, e:
1664 if e.code == 404:
1665 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1666 if e.code == 403:
1667 DieWithError(
1668 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1669 'match?') % (self.GetIssue(), self.GetPatchset()))
1670 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001671
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001672 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673 """Returns an upload.RpcServer() to access this review's rietveld instance.
1674 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001675 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001676 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001677 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001678 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001679 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001681 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001682 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001683 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001684
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 def PatchsetSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686 """Return the git setting that stores this change's most recent patchset."""
1687 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1688
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001689 def GetCodereviewServerSetting(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001691 branch = self.GetBranch()
1692 if branch:
1693 return 'branch.%s.rietveldserver' % branch
1694 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001695
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 def GetRieveldObjForPresubmit(self):
1697 return self.RpcServer()
1698
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001699 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1700 directory):
1701 # TODO(maruel): Use apply_issue.py
1702
1703 # PatchIssue should never be called with a dirty tree. It is up to the
1704 # caller to check this, but just in case we assert here since the
1705 # consequences of the caller not checking this could be dire.
1706 assert(not git_common.is_dirty_git_tree('apply'))
1707 assert(parsed_issue_arg.valid)
1708 self._changelist.issue = parsed_issue_arg.issue
1709 if parsed_issue_arg.hostname:
1710 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
1711
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001712 if (isinstance(parsed_issue_arg, _RietveldParsedIssueNumberArgument) and
1713 parsed_issue_arg.patch_url):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001714 assert parsed_issue_arg.patchset
1715 patchset = parsed_issue_arg.patchset
1716 patch_data = urllib2.urlopen(parsed_issue_arg.patch_url).read()
1717 else:
1718 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
1719 patch_data = self.GetPatchSetDiff(self.GetIssue(), patchset)
1720
1721 # Switch up to the top-level directory, if necessary, in preparation for
1722 # applying the patch.
1723 top = settings.GetRelativeRoot()
1724 if top:
1725 os.chdir(top)
1726
1727 # Git patches have a/ at the beginning of source paths. We strip that out
1728 # with a sed script rather than the -p flag to patch so we can feed either
1729 # Git or svn-style patches into the same apply command.
1730 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1731 try:
1732 patch_data = subprocess2.check_output(
1733 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1734 except subprocess2.CalledProcessError:
1735 DieWithError('Git patch mungling failed.')
1736 logging.info(patch_data)
1737
1738 # We use "git apply" to apply the patch instead of "patch" so that we can
1739 # pick up file adds.
1740 # The --index flag means: also insert into the index (so we catch adds).
1741 cmd = ['git', 'apply', '--index', '-p0']
1742 if directory:
1743 cmd.extend(('--directory', directory))
1744 if reject:
1745 cmd.append('--reject')
1746 elif IsGitVersionAtLeast('1.7.12'):
1747 cmd.append('--3way')
1748 try:
1749 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
1750 stdin=patch_data, stdout=subprocess2.VOID)
1751 except subprocess2.CalledProcessError:
1752 print 'Failed to apply the patch'
1753 return 1
1754
1755 # If we had an issue, commit the current state and register the issue.
1756 if not nocommit:
1757 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
1758 'patch from issue %(i)s at patchset '
1759 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
1760 % {'i': self.GetIssue(), 'p': patchset})])
1761 self.SetIssue(self.GetIssue())
1762 self.SetPatchset(patchset)
1763 print "Committed patch locally."
1764 else:
1765 print "Patch applied to index."
1766 return 0
1767
1768 @staticmethod
1769 def ParseIssueURL(parsed_url):
1770 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
1771 return None
1772 # Typical url: https://domain/<issue_number>[/[other]]
1773 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
1774 if match:
1775 return _RietveldParsedIssueNumberArgument(
1776 issue=int(match.group(1)),
1777 hostname=parsed_url.netloc)
1778 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
1779 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
1780 if match:
1781 return _RietveldParsedIssueNumberArgument(
1782 issue=int(match.group(1)),
1783 patchset=int(match.group(2)),
1784 hostname=parsed_url.netloc,
1785 patch_url=gclient_utils.UpgradeToHttps(parsed_url.geturl()))
1786 return None
1787
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001788 def CMDUploadChange(self, options, args, change):
1789 """Upload the patch to Rietveld."""
1790 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1791 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001792 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
1793 if options.emulate_svn_auto_props:
1794 upload_args.append('--emulate_svn_auto_props')
1795
1796 change_desc = None
1797
1798 if options.email is not None:
1799 upload_args.extend(['--email', options.email])
1800
1801 if self.GetIssue():
1802 if options.title:
1803 upload_args.extend(['--title', options.title])
1804 if options.message:
1805 upload_args.extend(['--message', options.message])
1806 upload_args.extend(['--issue', str(self.GetIssue())])
1807 print ('This branch is associated with issue %s. '
1808 'Adding patch to that issue.' % self.GetIssue())
1809 else:
1810 if options.title:
1811 upload_args.extend(['--title', options.title])
1812 message = (options.title or options.message or
1813 CreateDescriptionFromLog(args))
1814 change_desc = ChangeDescription(message)
1815 if options.reviewers or options.tbr_owners:
1816 change_desc.update_reviewers(options.reviewers,
1817 options.tbr_owners,
1818 change)
1819 if not options.force:
1820 change_desc.prompt()
1821
1822 if not change_desc.description:
1823 print "Description is empty; aborting."
1824 return 1
1825
1826 upload_args.extend(['--message', change_desc.description])
1827 if change_desc.get_reviewers():
1828 upload_args.append('--reviewers=%s' % ','.join(
1829 change_desc.get_reviewers()))
1830 if options.send_mail:
1831 if not change_desc.get_reviewers():
1832 DieWithError("Must specify reviewers to send email.")
1833 upload_args.append('--send_mail')
1834
1835 # We check this before applying rietveld.private assuming that in
1836 # rietveld.cc only addresses which we can send private CLs to are listed
1837 # if rietveld.private is set, and so we should ignore rietveld.cc only
1838 # when --private is specified explicitly on the command line.
1839 if options.private:
1840 logging.warn('rietveld.cc is ignored since private flag is specified. '
1841 'You need to review and add them manually if necessary.')
1842 cc = self.GetCCListWithoutDefault()
1843 else:
1844 cc = self.GetCCList()
1845 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
1846 if cc:
1847 upload_args.extend(['--cc', cc])
1848
1849 if options.private or settings.GetDefaultPrivateFlag() == "True":
1850 upload_args.append('--private')
1851
1852 upload_args.extend(['--git_similarity', str(options.similarity)])
1853 if not options.find_copies:
1854 upload_args.extend(['--git_no_find_copies'])
1855
1856 # Include the upstream repo's URL in the change -- this is useful for
1857 # projects that have their source spread across multiple repos.
1858 remote_url = self.GetGitBaseUrlFromConfig()
1859 if not remote_url:
1860 if settings.GetIsGitSvn():
1861 remote_url = self.GetGitSvnRemoteUrl()
1862 else:
1863 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
1864 remote_url = '%s@%s' % (self.GetRemoteUrl(),
1865 self.GetUpstreamBranch().split('/')[-1])
1866 if remote_url:
1867 upload_args.extend(['--base_url', remote_url])
1868 remote, remote_branch = self.GetRemoteBranch()
1869 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1870 settings.GetPendingRefPrefix())
1871 if target_ref:
1872 upload_args.extend(['--target_ref', target_ref])
1873
1874 # Look for dependent patchsets. See crbug.com/480453 for more details.
1875 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1876 upstream_branch = ShortBranchName(upstream_branch)
1877 if remote is '.':
1878 # A local branch is being tracked.
1879 local_branch = ShortBranchName(upstream_branch)
1880 if settings.GetIsSkipDependencyUpload(local_branch):
1881 print
1882 print ('Skipping dependency patchset upload because git config '
1883 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
1884 print
1885 else:
1886 auth_config = auth.extract_auth_config_from_options(options)
1887 branch_cl = Changelist(branchref=local_branch,
1888 auth_config=auth_config)
1889 branch_cl_issue_url = branch_cl.GetIssueURL()
1890 branch_cl_issue = branch_cl.GetIssue()
1891 branch_cl_patchset = branch_cl.GetPatchset()
1892 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
1893 upload_args.extend(
1894 ['--depends_on_patchset', '%s:%s' % (
1895 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001896 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001897 '\n'
1898 'The current branch (%s) is tracking a local branch (%s) with '
1899 'an associated CL.\n'
1900 'Adding %s/#ps%s as a dependency patchset.\n'
1901 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
1902 branch_cl_patchset))
1903
1904 project = settings.GetProject()
1905 if project:
1906 upload_args.extend(['--project', project])
1907
1908 if options.cq_dry_run:
1909 upload_args.extend(['--cq_dry_run'])
1910
1911 try:
1912 upload_args = ['upload'] + upload_args + args
1913 logging.info('upload.RealMain(%s)', upload_args)
1914 issue, patchset = upload.RealMain(upload_args)
1915 issue = int(issue)
1916 patchset = int(patchset)
1917 except KeyboardInterrupt:
1918 sys.exit(1)
1919 except:
1920 # If we got an exception after the user typed a description for their
1921 # change, back up the description before re-raising.
1922 if change_desc:
1923 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1924 print('\nGot exception while uploading -- saving description to %s\n' %
1925 backup_path)
1926 backup_file = open(backup_path, 'w')
1927 backup_file.write(change_desc.description)
1928 backup_file.close()
1929 raise
1930
1931 if not self.GetIssue():
1932 self.SetIssue(issue)
1933 self.SetPatchset(patchset)
1934
1935 if options.use_commit_queue:
1936 self.SetFlag('commit', '1')
1937 return 0
1938
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001939
1940class _GerritChangelistImpl(_ChangelistCodereviewBase):
1941 def __init__(self, changelist, auth_config=None):
1942 # auth_config is Rietveld thing, kept here to preserve interface only.
1943 super(_GerritChangelistImpl, self).__init__(changelist)
1944 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001945 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001946 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001947 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001948
1949 def _GetGerritHost(self):
1950 # Lazy load of configs.
1951 self.GetCodereviewServer()
1952 return self._gerrit_host
1953
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001954 def _GetGitHost(self):
1955 """Returns git host to be used when uploading change to Gerrit."""
1956 return urlparse.urlparse(self.GetRemoteUrl()).netloc
1957
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001958 def GetCodereviewServer(self):
1959 if not self._gerrit_server:
1960 # If we're on a branch then get the server potentially associated
1961 # with that branch.
1962 if self.GetIssue():
1963 gerrit_server_setting = self.GetCodereviewServerSetting()
1964 if gerrit_server_setting:
1965 self._gerrit_server = RunGit(['config', gerrit_server_setting],
1966 error_ok=True).strip()
1967 if self._gerrit_server:
1968 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
1969 if not self._gerrit_server:
1970 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1971 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001972 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973 parts[0] = parts[0] + '-review'
1974 self._gerrit_host = '.'.join(parts)
1975 self._gerrit_server = 'https://%s' % self._gerrit_host
1976 return self._gerrit_server
1977
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001978 @classmethod
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00001979 def IssueSettingSuffix(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001980 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001981
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001982 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001983 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001984 # Lazy-loader to identify Gerrit and Git hosts.
1985 if gerrit_util.GceAuthenticator.is_gce():
1986 return
1987 self.GetCodereviewServer()
1988 git_host = self._GetGitHost()
1989 assert self._gerrit_server and self._gerrit_host
1990 cookie_auth = gerrit_util.CookiesAuthenticator()
1991
1992 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1993 git_auth = cookie_auth.get_auth_header(git_host)
1994 if gerrit_auth and git_auth:
1995 if gerrit_auth == git_auth:
1996 return
1997 print((
1998 'WARNING: you have different credentials for Gerrit and git hosts.\n'
1999 ' Check your %s or %s file for credentials of hosts:\n'
2000 ' %s\n'
2001 ' %s\n'
2002 ' %s') %
2003 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2004 git_host, self._gerrit_host,
2005 cookie_auth.get_new_password_message(git_host)))
2006 if not force:
2007 ask_for_data('If you know what you are doing, press Enter to continue, '
2008 'Ctrl+C to abort.')
2009 return
2010 else:
2011 missing = (
2012 [] if gerrit_auth else [self._gerrit_host] +
2013 [] if git_auth else [git_host])
2014 DieWithError('Credentials for the following hosts are required:\n'
2015 ' %s\n'
2016 'These are read from %s (or legacy %s)\n'
2017 '%s' % (
2018 '\n '.join(missing),
2019 cookie_auth.get_gitcookies_path(),
2020 cookie_auth.get_netrc_path(),
2021 cookie_auth.get_new_password_message(git_host)))
2022
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002023
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002024 def PatchsetSetting(self):
2025 """Return the git setting that stores this change's most recent patchset."""
2026 return 'branch.%s.gerritpatchset' % self.GetBranch()
2027
2028 def GetCodereviewServerSetting(self):
2029 """Returns the git setting that stores this change's Gerrit server."""
2030 branch = self.GetBranch()
2031 if branch:
2032 return 'branch.%s.gerritserver' % branch
2033 return None
2034
2035 def GetRieveldObjForPresubmit(self):
2036 class ThisIsNotRietveldIssue(object):
2037 def __nonzero__(self):
2038 # This is a hack to make presubmit_support think that rietveld is not
2039 # defined, yet still ensure that calls directly result in a decent
2040 # exception message below.
2041 return False
2042
2043 def __getattr__(self, attr):
2044 print(
2045 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2046 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2047 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2048 'or use Rietveld for codereview.\n'
2049 'See also http://crbug.com/579160.' % attr)
2050 raise NotImplementedError()
2051 return ThisIsNotRietveldIssue()
2052
2053 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002054 """Apply a rough heuristic to give a simple summary of an issue's review
2055 or CQ status, assuming adherence to a common workflow.
2056
2057 Returns None if no issue for this branch, or one of the following keywords:
2058 * 'error' - error from review tool (including deleted issues)
2059 * 'unsent' - no reviewers added
2060 * 'waiting' - waiting for review
2061 * 'reply' - waiting for owner to reply to review
2062 * 'not lgtm' - Code-Review -2 from at least one approved reviewer
2063 * 'lgtm' - Code-Review +2 from at least one approved reviewer
2064 * 'commit' - in the commit queue
2065 * 'closed' - abandoned
2066 """
2067 if not self.GetIssue():
2068 return None
2069
2070 try:
2071 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
2072 except httplib.HTTPException:
2073 return 'error'
2074
2075 if data['status'] == 'ABANDONED':
2076 return 'closed'
2077
2078 cq_label = data['labels'].get('Commit-Queue', {})
2079 if cq_label:
2080 # Vote value is a stringified integer, which we expect from 0 to 2.
2081 vote_value = cq_label.get('value', '0')
2082 vote_text = cq_label.get('values', {}).get(vote_value, '')
2083 if vote_text.lower() == 'commit':
2084 return 'commit'
2085
2086 lgtm_label = data['labels'].get('Code-Review', {})
2087 if lgtm_label:
2088 if 'rejected' in lgtm_label:
2089 return 'not lgtm'
2090 if 'approved' in lgtm_label:
2091 return 'lgtm'
2092
2093 if not data.get('reviewers', {}).get('REVIEWER', []):
2094 return 'unsent'
2095
2096 messages = data.get('messages', [])
2097 if messages:
2098 owner = data['owner'].get('_account_id')
2099 last_message_author = messages[-1].get('author', {}).get('_account_id')
2100 if owner != last_message_author:
2101 # Some reply from non-owner.
2102 return 'reply'
2103
2104 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002105
2106 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002107 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002108 return data['revisions'][data['current_revision']]['_number']
2109
2110 def FetchDescription(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002111 data = self._GetChangeDetail(['COMMIT_FOOTERS', 'CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002112 return data['revisions'][data['current_revision']]['commit_with_footers']
2113
2114 def UpdateDescriptionRemote(self, description):
2115 # TODO(tandrii)
2116 raise NotImplementedError()
2117
2118 def CloseIssue(self):
2119 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2120
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002121 def SubmitIssue(self, wait_for_merge=True):
2122 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2123 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002125 def _GetChangeDetail(self, options=None, issue=None):
2126 options = options or []
2127 issue = issue or self.GetIssue()
2128 assert issue, 'issue required to query Gerrit'
tandrii@chromium.org11a899e2016-04-13 12:45:44 +00002129 return gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2130 options)
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002131
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002132 def CMDLand(self, force, bypass_hooks, verbose):
2133 if git_common.is_dirty_git_tree('land'):
2134 return 1
2135 differs = True
2136 last_upload = RunGit(['config',
2137 'branch.%s.gerritsquashhash' % self.GetBranch()],
2138 error_ok=True).strip()
2139 # Note: git diff outputs nothing if there is no diff.
2140 if not last_upload or RunGit(['diff', last_upload]).strip():
2141 print('WARNING: some changes from local branch haven\'t been uploaded')
2142 else:
2143 detail = self._GetChangeDetail(['CURRENT_REVISION'])
2144 if detail['current_revision'] == last_upload:
2145 differs = False
2146 else:
2147 print('WARNING: local branch contents differ from latest uploaded '
2148 'patchset')
2149 if differs:
2150 if not force:
2151 ask_for_data(
2152 'Do you want to submit latest Gerrit patchset and bypass hooks?')
2153 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2154 elif not bypass_hooks:
2155 hook_results = self.RunHook(
2156 committing=True,
2157 may_prompt=not force,
2158 verbose=verbose,
2159 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2160 if not hook_results.should_continue():
2161 return 1
2162
2163 self.SubmitIssue(wait_for_merge=True)
2164 print('Issue %s has been submitted.' % self.GetIssueURL())
2165 return 0
2166
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002167 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2168 directory):
2169 assert not reject
2170 assert not nocommit
2171 assert not directory
2172 assert parsed_issue_arg.valid
2173
2174 self._changelist.issue = parsed_issue_arg.issue
2175
2176 if parsed_issue_arg.hostname:
2177 self._gerrit_host = parsed_issue_arg.hostname
2178 self._gerrit_server = 'https://%s' % self._gerrit_host
2179
2180 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2181
2182 if not parsed_issue_arg.patchset:
2183 # Use current revision by default.
2184 revision_info = detail['revisions'][detail['current_revision']]
2185 patchset = int(revision_info['_number'])
2186 else:
2187 patchset = parsed_issue_arg.patchset
2188 for revision_info in detail['revisions'].itervalues():
2189 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2190 break
2191 else:
2192 DieWithError('Couldn\'t find patchset %i in issue %i' %
2193 (parsed_issue_arg.patchset, self.GetIssue()))
2194
2195 fetch_info = revision_info['fetch']['http']
2196 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2197 RunGit(['cherry-pick', 'FETCH_HEAD'])
2198 self.SetIssue(self.GetIssue())
2199 self.SetPatchset(patchset)
2200 print('Committed patch for issue %i pathset %i locally' %
2201 (self.GetIssue(), self.GetPatchset()))
2202 return 0
2203
2204 @staticmethod
2205 def ParseIssueURL(parsed_url):
2206 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2207 return None
2208 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2209 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2210 # Short urls like https://domain/<issue_number> can be used, but don't allow
2211 # specifying the patchset (you'd 404), but we allow that here.
2212 if parsed_url.path == '/':
2213 part = parsed_url.fragment
2214 else:
2215 part = parsed_url.path
2216 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2217 if match:
2218 return _ParsedIssueNumberArgument(
2219 issue=int(match.group(2)),
2220 patchset=int(match.group(4)) if match.group(4) else None,
2221 hostname=parsed_url.netloc)
2222 return None
2223
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002224 def CMDUploadChange(self, options, args, change):
2225 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002226 if options.squash and options.no_squash:
2227 DieWithError('Can only use one of --squash or --no-squash')
2228 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2229 not options.no_squash)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002230 # We assume the remote called "origin" is the one we want.
2231 # It is probably not worthwhile to support different workflows.
2232 gerrit_remote = 'origin'
2233
2234 remote, remote_branch = self.GetRemoteBranch()
2235 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2236 pending_prefix='')
2237
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002238 if options.squash:
2239 if not self.GetIssue():
2240 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2241 # with shadow branch, which used to contain change-id for a given
2242 # branch, using which we can fetch actual issue number and set it as the
2243 # property of the branch, which is the new way.
2244 message = RunGitSilent([
2245 'show', '--format=%B', '-s',
2246 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2247 if message:
2248 change_ids = git_footers.get_footer_change_id(message.strip())
2249 if change_ids and len(change_ids) == 1:
2250 details = self._GetChangeDetail(issue=change_ids[0])
2251 if details:
2252 print('WARNING: found old upload in branch git_cl_uploads/%s '
2253 'corresponding to issue %s' %
2254 (self.GetBranch(), details['_number']))
2255 self.SetIssue(details['_number'])
2256 if not self.GetIssue():
2257 DieWithError(
2258 '\n' # For readability of the blob below.
2259 'Found old upload in branch git_cl_uploads/%s, '
2260 'but failed to find corresponding Gerrit issue.\n'
2261 'If you know the issue number, set it manually first:\n'
2262 ' git cl issue 123456\n'
2263 'If you intended to upload this CL as new issue, '
2264 'just delete or rename the old upload branch:\n'
2265 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2266 'After that, please run git cl upload again.' %
2267 tuple([self.GetBranch()] * 3))
2268 # End of backwards compatability.
2269
2270 if self.GetIssue():
2271 # Try to get the message from a previous upload.
2272 message = self.GetDescription()
2273 if not message:
2274 DieWithError(
2275 'failed to fetch description from current Gerrit issue %d\n'
2276 '%s' % (self.GetIssue(), self.GetIssueURL()))
2277 change_id = self._GetChangeDetail()['change_id']
2278 while True:
2279 footer_change_ids = git_footers.get_footer_change_id(message)
2280 if footer_change_ids == [change_id]:
2281 break
2282 if not footer_change_ids:
2283 message = git_footers.add_footer_change_id(message, change_id)
2284 print('WARNING: appended missing Change-Id to issue description')
2285 continue
2286 # There is already a valid footer but with different or several ids.
2287 # Doing this automatically is non-trivial as we don't want to lose
2288 # existing other footers, yet we want to append just 1 desired
2289 # Change-Id. Thus, just create a new footer, but let user verify the
2290 # new description.
2291 message = '%s\n\nChange-Id: %s' % (message, change_id)
2292 print(
2293 'WARNING: issue %s has Change-Id footer(s):\n'
2294 ' %s\n'
2295 'but issue has Change-Id %s, according to Gerrit.\n'
2296 'Please, check the proposed correction to the description, '
2297 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2298 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2299 change_id))
2300 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2301 if not options.force:
2302 change_desc = ChangeDescription(message)
2303 change_desc.prompt()
2304 message = change_desc.description
2305 if not message:
2306 DieWithError("Description is empty. Aborting...")
2307 # Continue the while loop.
2308 # Sanity check of this code - we should end up with proper message
2309 # footer.
2310 assert [change_id] == git_footers.get_footer_change_id(message)
2311 change_desc = ChangeDescription(message)
2312 else:
2313 change_desc = ChangeDescription(
2314 options.message or CreateDescriptionFromLog(args))
2315 if not options.force:
2316 change_desc.prompt()
2317 if not change_desc.description:
2318 DieWithError("Description is empty. Aborting...")
2319 message = change_desc.description
2320 change_ids = git_footers.get_footer_change_id(message)
2321 if len(change_ids) > 1:
2322 DieWithError('too many Change-Id footers, at most 1 allowed.')
2323 if not change_ids:
2324 # Generate the Change-Id automatically.
2325 message = git_footers.add_footer_change_id(
2326 message, GenerateGerritChangeId(message))
2327 change_desc.set_description(message)
2328 change_ids = git_footers.get_footer_change_id(message)
2329 assert len(change_ids) == 1
2330 change_id = change_ids[0]
2331
2332 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2333 if remote is '.':
2334 # If our upstream branch is local, we base our squashed commit on its
2335 # squashed version.
2336 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2337 # Check the squashed hash of the parent.
2338 parent = RunGit(['config',
2339 'branch.%s.gerritsquashhash' % upstream_branch_name],
2340 error_ok=True).strip()
2341 # Verify that the upstream branch has been uploaded too, otherwise
2342 # Gerrit will create additional CLs when uploading.
2343 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2344 RunGitSilent(['rev-parse', parent + ':'])):
2345 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2346 DieWithError(
2347 'Upload upstream branch %s first.\n'
2348 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2349 'version of depot_tools. If so, then re-upload it with:\n'
2350 ' git cl upload --squash\n' % upstream_branch_name)
2351 else:
2352 parent = self.GetCommonAncestorWithUpstream()
2353
2354 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2355 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2356 '-m', message]).strip()
2357 else:
2358 change_desc = ChangeDescription(
2359 options.message or CreateDescriptionFromLog(args))
2360 if not change_desc.description:
2361 DieWithError("Description is empty. Aborting...")
2362
2363 if not git_footers.get_footer_change_id(change_desc.description):
2364 DownloadGerritHook(False)
2365 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
2366 ref_to_push = 'HEAD'
2367 parent = '%s/%s' % (gerrit_remote, branch)
2368 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2369
2370 assert change_desc
2371 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2372 ref_to_push)]).splitlines()
2373 if len(commits) > 1:
2374 print('WARNING: This will upload %d commits. Run the following command '
2375 'to see which commits will be uploaded: ' % len(commits))
2376 print('git log %s..%s' % (parent, ref_to_push))
2377 print('You can also use `git squash-branch` to squash these into a '
2378 'single commit.')
2379 ask_for_data('About to upload; enter to confirm.')
2380
2381 if options.reviewers or options.tbr_owners:
2382 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2383 change)
2384
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002385 # Extra options that can be specified at push time. Doc:
2386 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2387 refspec_opts = []
2388 if options.title:
2389 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2390 # reverse on its side.
2391 if '_' in options.title:
2392 print('WARNING: underscores in title will be converted to spaces.')
2393 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2394
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002395 receive_options = []
2396 cc = self.GetCCList().split(',')
2397 if options.cc:
2398 cc.extend(options.cc)
2399 cc = filter(None, cc)
2400 if cc:
2401 receive_options += ['--cc=' + email for email in cc]
2402 if change_desc.get_reviewers():
2403 receive_options.extend(
2404 '--reviewer=' + email for email in change_desc.get_reviewers())
2405
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002406 git_command = ['git', 'push']
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002407 if receive_options:
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002408 # TODO(tandrii): clean this up in follow up. This doesn't work, as it gets
2409 # totally ignored by Gerrit.
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002410 git_command.append('--receive-pack=git receive-pack %s' %
2411 ' '.join(receive_options))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002412
2413 refspec_suffix = ''
2414 if refspec_opts:
2415 refspec_suffix = '%' + ','.join(refspec_opts)
2416 assert ' ' not in refspec_suffix, (
2417 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2418
2419 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2420 git_command += [gerrit_remote, refspec]
2421
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002422 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002423 git_command,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002424 print_stdout=True,
2425 # Flush after every line: useful for seeing progress when running as
2426 # recipe.
2427 filter_fn=lambda _: sys.stdout.flush())
2428
2429 if options.squash:
2430 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2431 change_numbers = [m.group(1)
2432 for m in map(regex.match, push_stdout.splitlines())
2433 if m]
2434 if len(change_numbers) != 1:
2435 DieWithError(
2436 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2437 'Change-Id: %s') % (len(change_numbers), change_id))
2438 self.SetIssue(change_numbers[0])
2439 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2440 ref_to_push])
2441 return 0
2442
2443
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002444
2445_CODEREVIEW_IMPLEMENTATIONS = {
2446 'rietveld': _RietveldChangelistImpl,
2447 'gerrit': _GerritChangelistImpl,
2448}
2449
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002450
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002451class ChangeDescription(object):
2452 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002453 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002454 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002455
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002456 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002457 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002458
agable@chromium.org42c20792013-09-12 17:34:49 +00002459 @property # www.logilab.org/ticket/89786
2460 def description(self): # pylint: disable=E0202
2461 return '\n'.join(self._description_lines)
2462
2463 def set_description(self, desc):
2464 if isinstance(desc, basestring):
2465 lines = desc.splitlines()
2466 else:
2467 lines = [line.rstrip() for line in desc]
2468 while lines and not lines[0]:
2469 lines.pop(0)
2470 while lines and not lines[-1]:
2471 lines.pop(-1)
2472 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002473
piman@chromium.org336f9122014-09-04 02:16:55 +00002474 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002475 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002476 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002477 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002478 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002479 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002480
agable@chromium.org42c20792013-09-12 17:34:49 +00002481 # Get the set of R= and TBR= lines and remove them from the desciption.
2482 regexp = re.compile(self.R_LINE)
2483 matches = [regexp.match(line) for line in self._description_lines]
2484 new_desc = [l for i, l in enumerate(self._description_lines)
2485 if not matches[i]]
2486 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002487
agable@chromium.org42c20792013-09-12 17:34:49 +00002488 # Construct new unified R= and TBR= lines.
2489 r_names = []
2490 tbr_names = []
2491 for match in matches:
2492 if not match:
2493 continue
2494 people = cleanup_list([match.group(2).strip()])
2495 if match.group(1) == 'TBR':
2496 tbr_names.extend(people)
2497 else:
2498 r_names.extend(people)
2499 for name in r_names:
2500 if name not in reviewers:
2501 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002502 if add_owners_tbr:
2503 owners_db = owners.Database(change.RepositoryRoot(),
2504 fopen=file, os_path=os.path, glob=glob.glob)
2505 all_reviewers = set(tbr_names + reviewers)
2506 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2507 all_reviewers)
2508 tbr_names.extend(owners_db.reviewers_for(missing_files,
2509 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002510 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2511 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2512
2513 # Put the new lines in the description where the old first R= line was.
2514 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2515 if 0 <= line_loc < len(self._description_lines):
2516 if new_tbr_line:
2517 self._description_lines.insert(line_loc, new_tbr_line)
2518 if new_r_line:
2519 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002520 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002521 if new_r_line:
2522 self.append_footer(new_r_line)
2523 if new_tbr_line:
2524 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002525
2526 def prompt(self):
2527 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002528 self.set_description([
2529 '# Enter a description of the change.',
2530 '# This will be displayed on the codereview site.',
2531 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002532 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002533 '--------------------',
2534 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002535
agable@chromium.org42c20792013-09-12 17:34:49 +00002536 regexp = re.compile(self.BUG_LINE)
2537 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002538 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002539 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002540 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002541 if not content:
2542 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002543 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002544
2545 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002546 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2547 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002548 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002549 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002550
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002551 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002552 if self._description_lines:
2553 # Add an empty line if either the last line or the new line isn't a tag.
2554 last_line = self._description_lines[-1]
2555 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2556 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2557 self._description_lines.append('')
2558 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002559
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002560 def get_reviewers(self):
2561 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002562 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2563 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002564 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002565
2566
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002567def get_approving_reviewers(props):
2568 """Retrieves the reviewers that approved a CL from the issue properties with
2569 messages.
2570
2571 Note that the list may contain reviewers that are not committer, thus are not
2572 considered by the CQ.
2573 """
2574 return sorted(
2575 set(
2576 message['sender']
2577 for message in props['messages']
2578 if message['approval'] and message['sender'] in props['reviewers']
2579 )
2580 )
2581
2582
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002583def FindCodereviewSettingsFile(filename='codereview.settings'):
2584 """Finds the given file starting in the cwd and going up.
2585
2586 Only looks up to the top of the repository unless an
2587 'inherit-review-settings-ok' file exists in the root of the repository.
2588 """
2589 inherit_ok_file = 'inherit-review-settings-ok'
2590 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002591 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002592 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2593 root = '/'
2594 while True:
2595 if filename in os.listdir(cwd):
2596 if os.path.isfile(os.path.join(cwd, filename)):
2597 return open(os.path.join(cwd, filename))
2598 if cwd == root:
2599 break
2600 cwd = os.path.dirname(cwd)
2601
2602
2603def LoadCodereviewSettingsFromFile(fileobj):
2604 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002605 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002607 def SetProperty(name, setting, unset_error_ok=False):
2608 fullname = 'rietveld.' + name
2609 if setting in keyvals:
2610 RunGit(['config', fullname, keyvals[setting]])
2611 else:
2612 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2613
2614 SetProperty('server', 'CODE_REVIEW_SERVER')
2615 # Only server setting is required. Other settings can be absent.
2616 # In that case, we ignore errors raised during option deletion attempt.
2617 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002618 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002619 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2620 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002621 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002622 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002623 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2624 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002625 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002626 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002627 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002628 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2629 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002630
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002631 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002632 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002633
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002634 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2635 RunGit(['config', 'gerrit.squash-uploads',
2636 keyvals['GERRIT_SQUASH_UPLOADS']])
2637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002638 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2639 #should be of the form
2640 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2641 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2642 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2643 keyvals['ORIGIN_URL_CONFIG']])
2644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002645
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002646def urlretrieve(source, destination):
2647 """urllib is broken for SSL connections via a proxy therefore we
2648 can't use urllib.urlretrieve()."""
2649 with open(destination, 'w') as f:
2650 f.write(urllib2.urlopen(source).read())
2651
2652
ukai@chromium.org712d6102013-11-27 00:52:58 +00002653def hasSheBang(fname):
2654 """Checks fname is a #! script."""
2655 with open(fname) as f:
2656 return f.read(2).startswith('#!')
2657
2658
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002659# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2660def DownloadHooks(*args, **kwargs):
2661 pass
2662
2663
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002664def DownloadGerritHook(force):
2665 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002666
2667 Args:
2668 force: True to update hooks. False to install hooks if not present.
2669 """
2670 if not settings.GetIsGerrit():
2671 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002672 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002673 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2674 if not os.access(dst, os.X_OK):
2675 if os.path.exists(dst):
2676 if not force:
2677 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002678 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002679 print(
2680 'WARNING: installing Gerrit commit-msg hook.\n'
2681 ' This behavior of git cl will soon be disabled.\n'
2682 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002683 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002684 if not hasSheBang(dst):
2685 DieWithError('Not a script: %s\n'
2686 'You need to download from\n%s\n'
2687 'into .git/hooks/commit-msg and '
2688 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002689 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2690 except Exception:
2691 if os.path.exists(dst):
2692 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002693 DieWithError('\nFailed to download hooks.\n'
2694 'You need to download from\n%s\n'
2695 'into .git/hooks/commit-msg and '
2696 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002697
2698
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002699
2700def GetRietveldCodereviewSettingsInteractively():
2701 """Prompt the user for settings."""
2702 server = settings.GetDefaultServerUrl(error_ok=True)
2703 prompt = 'Rietveld server (host[:port])'
2704 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2705 newserver = ask_for_data(prompt + ':')
2706 if not server and not newserver:
2707 newserver = DEFAULT_SERVER
2708 if newserver:
2709 newserver = gclient_utils.UpgradeToHttps(newserver)
2710 if newserver != server:
2711 RunGit(['config', 'rietveld.server', newserver])
2712
2713 def SetProperty(initial, caption, name, is_url):
2714 prompt = caption
2715 if initial:
2716 prompt += ' ("x" to clear) [%s]' % initial
2717 new_val = ask_for_data(prompt + ':')
2718 if new_val == 'x':
2719 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2720 elif new_val:
2721 if is_url:
2722 new_val = gclient_utils.UpgradeToHttps(new_val)
2723 if new_val != initial:
2724 RunGit(['config', 'rietveld.' + name, new_val])
2725
2726 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2727 SetProperty(settings.GetDefaultPrivateFlag(),
2728 'Private flag (rietveld only)', 'private', False)
2729 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2730 'tree-status-url', False)
2731 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2732 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2733 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2734 'run-post-upload-hook', False)
2735
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002736@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002737def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002738 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002739
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002740 print('WARNING: git cl config works for Rietveld only.\n'
2741 'For Gerrit, see http://crbug.com/579160.')
2742 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002743 parser.add_option('--activate-update', action='store_true',
2744 help='activate auto-updating [rietveld] section in '
2745 '.git/config')
2746 parser.add_option('--deactivate-update', action='store_true',
2747 help='deactivate auto-updating [rietveld] section in '
2748 '.git/config')
2749 options, args = parser.parse_args(args)
2750
2751 if options.deactivate_update:
2752 RunGit(['config', 'rietveld.autoupdate', 'false'])
2753 return
2754
2755 if options.activate_update:
2756 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2757 return
2758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002759 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002760 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761 return 0
2762
2763 url = args[0]
2764 if not url.endswith('codereview.settings'):
2765 url = os.path.join(url, 'codereview.settings')
2766
2767 # Load code review settings and download hooks (if available).
2768 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2769 return 0
2770
2771
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002772def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002773 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002774 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2775 branch = ShortBranchName(branchref)
2776 _, args = parser.parse_args(args)
2777 if not args:
2778 print("Current base-url:")
2779 return RunGit(['config', 'branch.%s.base-url' % branch],
2780 error_ok=False).strip()
2781 else:
2782 print("Setting base-url to %s" % args[0])
2783 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2784 error_ok=False).strip()
2785
2786
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002787def color_for_status(status):
2788 """Maps a Changelist status to color, for CMDstatus and other tools."""
2789 return {
2790 'unsent': Fore.RED,
2791 'waiting': Fore.BLUE,
2792 'reply': Fore.YELLOW,
2793 'lgtm': Fore.GREEN,
2794 'commit': Fore.MAGENTA,
2795 'closed': Fore.CYAN,
2796 'error': Fore.WHITE,
2797 }.get(status, Fore.WHITE)
2798
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002799def fetch_cl_status(branch, auth_config=None):
2800 """Fetches information for an issue and returns (branch, issue, status)."""
2801 cl = Changelist(branchref=branch, auth_config=auth_config)
2802 url = cl.GetIssueURL()
2803 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002804
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002805 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002806 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002807 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002808
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002809 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002810
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002811def get_cl_statuses(
2812 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002813 """Returns a blocking iterable of (branch, issue, color) for given branches.
2814
2815 If fine_grained is true, this will fetch CL statuses from the server.
2816 Otherwise, simply indicate if there's a matching url for the given branches.
2817
2818 If max_processes is specified, it is used as the maximum number of processes
2819 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2820 spawned.
2821 """
2822 # Silence upload.py otherwise it becomes unwieldly.
2823 upload.verbosity = 0
2824
2825 if fine_grained:
2826 # Process one branch synchronously to work through authentication, then
2827 # spawn processes to process all the other branches in parallel.
2828 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002829 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2830 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002831
2832 branches_to_fetch = branches[1:]
2833 pool = ThreadPool(
2834 min(max_processes, len(branches_to_fetch))
2835 if max_processes is not None
2836 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002837 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002838 yield x
2839 else:
2840 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2841 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002842 cl = Changelist(branchref=b, auth_config=auth_config)
2843 url = cl.GetIssueURL()
2844 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002845
rmistry@google.com2dd99862015-06-22 12:22:18 +00002846
2847def upload_branch_deps(cl, args):
2848 """Uploads CLs of local branches that are dependents of the current branch.
2849
2850 If the local branch dependency tree looks like:
2851 test1 -> test2.1 -> test3.1
2852 -> test3.2
2853 -> test2.2 -> test3.3
2854
2855 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2856 run on the dependent branches in this order:
2857 test2.1, test3.1, test3.2, test2.2, test3.3
2858
2859 Note: This function does not rebase your local dependent branches. Use it when
2860 you make a change to the parent branch that will not conflict with its
2861 dependent branches, and you would like their dependencies updated in
2862 Rietveld.
2863 """
2864 if git_common.is_dirty_git_tree('upload-branch-deps'):
2865 return 1
2866
2867 root_branch = cl.GetBranch()
2868 if root_branch is None:
2869 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2870 'Get on a branch!')
2871 if not cl.GetIssue() or not cl.GetPatchset():
2872 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2873 'patchset dependencies without an uploaded CL.')
2874
2875 branches = RunGit(['for-each-ref',
2876 '--format=%(refname:short) %(upstream:short)',
2877 'refs/heads'])
2878 if not branches:
2879 print('No local branches found.')
2880 return 0
2881
2882 # Create a dictionary of all local branches to the branches that are dependent
2883 # on it.
2884 tracked_to_dependents = collections.defaultdict(list)
2885 for b in branches.splitlines():
2886 tokens = b.split()
2887 if len(tokens) == 2:
2888 branch_name, tracked = tokens
2889 tracked_to_dependents[tracked].append(branch_name)
2890
2891 print
2892 print 'The dependent local branches of %s are:' % root_branch
2893 dependents = []
2894 def traverse_dependents_preorder(branch, padding=''):
2895 dependents_to_process = tracked_to_dependents.get(branch, [])
2896 padding += ' '
2897 for dependent in dependents_to_process:
2898 print '%s%s' % (padding, dependent)
2899 dependents.append(dependent)
2900 traverse_dependents_preorder(dependent, padding)
2901 traverse_dependents_preorder(root_branch)
2902 print
2903
2904 if not dependents:
2905 print 'There are no dependent local branches for %s' % root_branch
2906 return 0
2907
2908 print ('This command will checkout all dependent branches and run '
2909 '"git cl upload".')
2910 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2911
andybons@chromium.org962f9462016-02-03 20:00:42 +00002912 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002913 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002914 args.extend(['-t', 'Updated patchset dependency'])
2915
rmistry@google.com2dd99862015-06-22 12:22:18 +00002916 # Record all dependents that failed to upload.
2917 failures = {}
2918 # Go through all dependents, checkout the branch and upload.
2919 try:
2920 for dependent_branch in dependents:
2921 print
2922 print '--------------------------------------'
2923 print 'Running "git cl upload" from %s:' % dependent_branch
2924 RunGit(['checkout', '-q', dependent_branch])
2925 print
2926 try:
2927 if CMDupload(OptionParser(), args) != 0:
2928 print 'Upload failed for %s!' % dependent_branch
2929 failures[dependent_branch] = 1
2930 except: # pylint: disable=W0702
2931 failures[dependent_branch] = 1
2932 print
2933 finally:
2934 # Swap back to the original root branch.
2935 RunGit(['checkout', '-q', root_branch])
2936
2937 print
2938 print 'Upload complete for dependent branches!'
2939 for dependent_branch in dependents:
2940 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2941 print ' %s : %s' % (dependent_branch, upload_status)
2942 print
2943
2944 return 0
2945
2946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002948 """Show status of changelists.
2949
2950 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002951 - Red not sent for review or broken
2952 - Blue waiting for review
2953 - Yellow waiting for you to reply to review
2954 - Green LGTM'ed
2955 - Magenta in the commit queue
2956 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002957
2958 Also see 'git cl comments'.
2959 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002960 parser.add_option('--field',
2961 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002962 parser.add_option('-f', '--fast', action='store_true',
2963 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002964 parser.add_option(
2965 '-j', '--maxjobs', action='store', type=int,
2966 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002967
2968 auth.add_auth_options(parser)
2969 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002970 if args:
2971 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002972 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002974 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002975 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002976 if options.field.startswith('desc'):
2977 print cl.GetDescription()
2978 elif options.field == 'id':
2979 issueid = cl.GetIssue()
2980 if issueid:
2981 print issueid
2982 elif options.field == 'patch':
2983 patchset = cl.GetPatchset()
2984 if patchset:
2985 print patchset
2986 elif options.field == 'url':
2987 url = cl.GetIssueURL()
2988 if url:
2989 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002990 return 0
2991
2992 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2993 if not branches:
2994 print('No local branch found.')
2995 return 0
2996
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002997 changes = (
2998 Changelist(branchref=b, auth_config=auth_config)
2999 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003000 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00003001 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003002 alignment = max(5, max(len(b) for b in branches))
3003 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003004 output = get_cl_statuses(branches,
3005 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003006 max_processes=options.maxjobs,
3007 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003008
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003009 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003010 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003011 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003012 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003013 b, i, status = output.next()
3014 branch_statuses[b] = (i, status)
3015 issue_url, status = branch_statuses.pop(branch)
3016 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003017 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003018 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003019 color = ''
3020 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003021 status_str = '(%s)' % status if status else ''
3022 print ' %*s : %s%s %s%s' % (
3023 alignment, ShortBranchName(branch), color, issue_url, status_str,
3024 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003025
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003026 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003027 print
3028 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003029 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003030 if not cl.GetIssue():
3031 print 'No issue assigned.'
3032 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003033 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003034 if not options.fast:
3035 print 'Issue description:'
3036 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003037 return 0
3038
3039
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003040def colorize_CMDstatus_doc():
3041 """To be called once in main() to add colors to git cl status help."""
3042 colors = [i for i in dir(Fore) if i[0].isupper()]
3043
3044 def colorize_line(line):
3045 for color in colors:
3046 if color in line.upper():
3047 # Extract whitespaces first and the leading '-'.
3048 indent = len(line) - len(line.lstrip(' ')) + 1
3049 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3050 return line
3051
3052 lines = CMDstatus.__doc__.splitlines()
3053 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3054
3055
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003056@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003057def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003058 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059
3060 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003061 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003062 parser.add_option('-r', '--reverse', action='store_true',
3063 help='Lookup the branch(es) for the specified issues. If '
3064 'no issues are specified, all branches with mapped '
3065 'issues will be listed.')
3066 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003067
dnj@chromium.org406c4402015-03-03 17:22:28 +00003068 if options.reverse:
3069 branches = RunGit(['for-each-ref', 'refs/heads',
3070 '--format=%(refname:short)']).splitlines()
3071
3072 # Reverse issue lookup.
3073 issue_branch_map = {}
3074 for branch in branches:
3075 cl = Changelist(branchref=branch)
3076 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3077 if not args:
3078 args = sorted(issue_branch_map.iterkeys())
3079 for issue in args:
3080 if not issue:
3081 continue
3082 print 'Branch for issue number %s: %s' % (
3083 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3084 else:
3085 cl = Changelist()
3086 if len(args) > 0:
3087 try:
3088 issue = int(args[0])
3089 except ValueError:
3090 DieWithError('Pass a number to set the issue or none to list it.\n'
3091 'Maybe you want to run git cl status?')
3092 cl.SetIssue(issue)
3093 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003094 return 0
3095
3096
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003097def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003098 """Shows or posts review comments for any changelist."""
3099 parser.add_option('-a', '--add-comment', dest='comment',
3100 help='comment to add to an issue')
3101 parser.add_option('-i', dest='issue',
3102 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003103 parser.add_option('-j', '--json-file',
3104 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003105 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003106 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003107 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003108
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003109 issue = None
3110 if options.issue:
3111 try:
3112 issue = int(options.issue)
3113 except ValueError:
3114 DieWithError('A review issue id is expected to be a number')
3115
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003116 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003117
3118 if options.comment:
3119 cl.AddComment(options.comment)
3120 return 0
3121
3122 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003123 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003124 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003125 summary.append({
3126 'date': message['date'],
3127 'lgtm': False,
3128 'message': message['text'],
3129 'not_lgtm': False,
3130 'sender': message['sender'],
3131 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003132 if message['disapproval']:
3133 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003134 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003135 elif message['approval']:
3136 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003137 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003138 elif message['sender'] == data['owner_email']:
3139 color = Fore.MAGENTA
3140 else:
3141 color = Fore.BLUE
3142 print '\n%s%s %s%s' % (
3143 color, message['date'].split('.', 1)[0], message['sender'],
3144 Fore.RESET)
3145 if message['text'].strip():
3146 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003147 if options.json_file:
3148 with open(options.json_file, 'wb') as f:
3149 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003150 return 0
3151
3152
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003153def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003154 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003155 parser.add_option('-d', '--display', action='store_true',
3156 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003157 auth.add_auth_options(parser)
3158 options, _ = parser.parse_args(args)
3159 auth_config = auth.extract_auth_config_from_options(options)
3160 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003161 if not cl.GetIssue():
3162 DieWithError('This branch has no associated changelist.')
3163 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003164 if options.display:
3165 print description.description
3166 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003167 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003168 if cl.GetDescription() != description.description:
3169 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003170 return 0
3171
3172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003173def CreateDescriptionFromLog(args):
3174 """Pulls out the commit log to use as a base for the CL description."""
3175 log_args = []
3176 if len(args) == 1 and not args[0].endswith('.'):
3177 log_args = [args[0] + '..']
3178 elif len(args) == 1 and args[0].endswith('...'):
3179 log_args = [args[0][:-1]]
3180 elif len(args) == 2:
3181 log_args = [args[0] + '..' + args[1]]
3182 else:
3183 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003184 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003185
3186
thestig@chromium.org44202a22014-03-11 19:22:18 +00003187def CMDlint(parser, args):
3188 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003189 parser.add_option('--filter', action='append', metavar='-x,+y',
3190 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003191 auth.add_auth_options(parser)
3192 options, args = parser.parse_args(args)
3193 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003194
3195 # Access to a protected member _XX of a client class
3196 # pylint: disable=W0212
3197 try:
3198 import cpplint
3199 import cpplint_chromium
3200 except ImportError:
3201 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3202 return 1
3203
3204 # Change the current working directory before calling lint so that it
3205 # shows the correct base.
3206 previous_cwd = os.getcwd()
3207 os.chdir(settings.GetRoot())
3208 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003209 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003210 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3211 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003212 if not files:
3213 print "Cannot lint an empty CL"
3214 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003215
3216 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003217 command = args + files
3218 if options.filter:
3219 command = ['--filter=' + ','.join(options.filter)] + command
3220 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003221
3222 white_regex = re.compile(settings.GetLintRegex())
3223 black_regex = re.compile(settings.GetLintIgnoreRegex())
3224 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3225 for filename in filenames:
3226 if white_regex.match(filename):
3227 if black_regex.match(filename):
3228 print "Ignoring file %s" % filename
3229 else:
3230 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3231 extra_check_functions)
3232 else:
3233 print "Skipping file %s" % filename
3234 finally:
3235 os.chdir(previous_cwd)
3236 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3237 if cpplint._cpplint_state.error_count != 0:
3238 return 1
3239 return 0
3240
3241
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003242def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003243 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003244 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003245 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003246 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003247 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003248 auth.add_auth_options(parser)
3249 options, args = parser.parse_args(args)
3250 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003251
sbc@chromium.org71437c02015-04-09 19:29:40 +00003252 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003253 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254 return 1
3255
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003256 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257 if args:
3258 base_branch = args[0]
3259 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003260 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003261 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003263 cl.RunHook(
3264 committing=not options.upload,
3265 may_prompt=False,
3266 verbose=options.verbose,
3267 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003268 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003269
3270
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003271def AddChangeIdToCommitMessage(options, args):
3272 """Re-commits using the current message, assumes the commit hook is in
3273 place.
3274 """
3275 log_desc = options.message or CreateDescriptionFromLog(args)
3276 git_command = ['commit', '--amend', '-m', log_desc]
3277 RunGit(git_command)
3278 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003279 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003280 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00003281 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003282 else:
3283 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
3284
3285
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003286def GenerateGerritChangeId(message):
3287 """Returns Ixxxxxx...xxx change id.
3288
3289 Works the same way as
3290 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3291 but can be called on demand on all platforms.
3292
3293 The basic idea is to generate git hash of a state of the tree, original commit
3294 message, author/committer info and timestamps.
3295 """
3296 lines = []
3297 tree_hash = RunGitSilent(['write-tree'])
3298 lines.append('tree %s' % tree_hash.strip())
3299 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3300 if code == 0:
3301 lines.append('parent %s' % parent.strip())
3302 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3303 lines.append('author %s' % author.strip())
3304 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3305 lines.append('committer %s' % committer.strip())
3306 lines.append('')
3307 # Note: Gerrit's commit-hook actually cleans message of some lines and
3308 # whitespace. This code is not doing this, but it clearly won't decrease
3309 # entropy.
3310 lines.append(message)
3311 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3312 stdin='\n'.join(lines))
3313 return 'I%s' % change_hash.strip()
3314
3315
wittman@chromium.org455dc922015-01-26 20:15:50 +00003316def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3317 """Computes the remote branch ref to use for the CL.
3318
3319 Args:
3320 remote (str): The git remote for the CL.
3321 remote_branch (str): The git remote branch for the CL.
3322 target_branch (str): The target branch specified by the user.
3323 pending_prefix (str): The pending prefix from the settings.
3324 """
3325 if not (remote and remote_branch):
3326 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003327
wittman@chromium.org455dc922015-01-26 20:15:50 +00003328 if target_branch:
3329 # Cannonicalize branch references to the equivalent local full symbolic
3330 # refs, which are then translated into the remote full symbolic refs
3331 # below.
3332 if '/' not in target_branch:
3333 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3334 else:
3335 prefix_replacements = (
3336 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3337 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3338 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3339 )
3340 match = None
3341 for regex, replacement in prefix_replacements:
3342 match = re.search(regex, target_branch)
3343 if match:
3344 remote_branch = target_branch.replace(match.group(0), replacement)
3345 break
3346 if not match:
3347 # This is a branch path but not one we recognize; use as-is.
3348 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003349 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3350 # Handle the refs that need to land in different refs.
3351 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003352
wittman@chromium.org455dc922015-01-26 20:15:50 +00003353 # Create the true path to the remote branch.
3354 # Does the following translation:
3355 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3356 # * refs/remotes/origin/master -> refs/heads/master
3357 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3358 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3359 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3360 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3361 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3362 'refs/heads/')
3363 elif remote_branch.startswith('refs/remotes/branch-heads'):
3364 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3365 # If a pending prefix exists then replace refs/ with it.
3366 if pending_prefix:
3367 remote_branch = remote_branch.replace('refs/', pending_prefix)
3368 return remote_branch
3369
3370
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003371def cleanup_list(l):
3372 """Fixes a list so that comma separated items are put as individual items.
3373
3374 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3375 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3376 """
3377 items = sum((i.split(',') for i in l), [])
3378 stripped_items = (i.strip() for i in items)
3379 return sorted(filter(None, stripped_items))
3380
3381
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003382@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003383def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003384 """Uploads the current changelist to codereview.
3385
3386 Can skip dependency patchset uploads for a branch by running:
3387 git config branch.branch_name.skip-deps-uploads True
3388 To unset run:
3389 git config --unset branch.branch_name.skip-deps-uploads
3390 Can also set the above globally by using the --global flag.
3391 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003392 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3393 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003394 parser.add_option('--bypass-watchlists', action='store_true',
3395 dest='bypass_watchlists',
3396 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003397 parser.add_option('-f', action='store_true', dest='force',
3398 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003399 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003400 parser.add_option('-t', dest='title',
3401 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003402 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003403 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003404 help='reviewer email addresses')
3405 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003406 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003407 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003408 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003409 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003410 parser.add_option('--emulate_svn_auto_props',
3411 '--emulate-svn-auto-props',
3412 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003413 dest="emulate_svn_auto_props",
3414 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003415 parser.add_option('-c', '--use-commit-queue', action='store_true',
3416 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003417 parser.add_option('--private', action='store_true',
3418 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003419 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003420 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003421 metavar='TARGET',
3422 help='Apply CL to remote ref TARGET. ' +
3423 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003424 parser.add_option('--squash', action='store_true',
3425 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003426 parser.add_option('--no-squash', action='store_true',
3427 help='Don\'t squash multiple commits into one ' +
3428 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003429 parser.add_option('--email', default=None,
3430 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003431 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3432 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003433 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3434 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003435 help='Send the patchset to do a CQ dry run right after '
3436 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003437 parser.add_option('--dependencies', action='store_true',
3438 help='Uploads CLs of all the local branches that depend on '
3439 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003440
rmistry@google.com2dd99862015-06-22 12:22:18 +00003441 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003442 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003443 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003444 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003445 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003446
sbc@chromium.org71437c02015-04-09 19:29:40 +00003447 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003448 return 1
3449
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003450 options.reviewers = cleanup_list(options.reviewers)
3451 options.cc = cleanup_list(options.cc)
3452
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003453 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3454 settings.GetIsGerrit()
3455
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003456 cl = Changelist(auth_config=auth_config)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003457 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003458
3459
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003460def IsSubmoduleMergeCommit(ref):
3461 # When submodules are added to the repo, we expect there to be a single
3462 # non-git-svn merge commit at remote HEAD with a signature comment.
3463 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003464 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003465 return RunGit(cmd) != ''
3466
3467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003468def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003469 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003470
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003471 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3472 upstream and closes the issue automatically and atomically.
3473
3474 Otherwise (in case of Rietveld):
3475 Squashes branch into a single commit.
3476 Updates changelog with metadata (e.g. pointer to review).
3477 Pushes/dcommits the code upstream.
3478 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003479 """
3480 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3481 help='bypass upload presubmit hook')
3482 parser.add_option('-m', dest='message',
3483 help="override review description")
3484 parser.add_option('-f', action='store_true', dest='force',
3485 help="force yes to questions (don't prompt)")
3486 parser.add_option('-c', dest='contributor',
3487 help="external contributor for patch (appended to " +
3488 "description and used as author for git). Should be " +
3489 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003490 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003491 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003492 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003493 auth_config = auth.extract_auth_config_from_options(options)
3494
3495 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003496
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003497 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3498 if cl.IsGerrit():
3499 if options.message:
3500 # This could be implemented, but it requires sending a new patch to
3501 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3502 # Besides, Gerrit has the ability to change the commit message on submit
3503 # automatically, thus there is no need to support this option (so far?).
3504 parser.error('-m MESSAGE option is not supported for Gerrit.')
3505 if options.contributor:
3506 parser.error(
3507 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3508 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3509 'the contributor\'s "name <email>". If you can\'t upload such a '
3510 'commit for review, contact your repository admin and request'
3511 '"Forge-Author" permission.')
3512 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3513 options.verbose)
3514
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003515 current = cl.GetBranch()
3516 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3517 if not settings.GetIsGitSvn() and remote == '.':
3518 print
3519 print 'Attempting to push branch %r into another local branch!' % current
3520 print
3521 print 'Either reparent this branch on top of origin/master:'
3522 print ' git reparent-branch --root'
3523 print
3524 print 'OR run `git rebase-update` if you think the parent branch is already'
3525 print 'committed.'
3526 print
3527 print ' Current parent: %r' % upstream_branch
3528 return 1
3529
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003530 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003531 # Default to merging against our best guess of the upstream branch.
3532 args = [cl.GetUpstreamBranch()]
3533
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003534 if options.contributor:
3535 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3536 print "Please provide contibutor as 'First Last <email@example.com>'"
3537 return 1
3538
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003539 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003540 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003541
sbc@chromium.org71437c02015-04-09 19:29:40 +00003542 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003543 return 1
3544
3545 # This rev-list syntax means "show all commits not in my branch that
3546 # are in base_branch".
3547 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3548 base_branch]).splitlines()
3549 if upstream_commits:
3550 print ('Base branch "%s" has %d commits '
3551 'not in this branch.' % (base_branch, len(upstream_commits)))
3552 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3553 return 1
3554
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003555 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003556 svn_head = None
3557 if cmd == 'dcommit' or base_has_submodules:
3558 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3559 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003560
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003562 # If the base_head is a submodule merge commit, the first parent of the
3563 # base_head should be a git-svn commit, which is what we're interested in.
3564 base_svn_head = base_branch
3565 if base_has_submodules:
3566 base_svn_head += '^1'
3567
3568 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003569 if extra_commits:
3570 print ('This branch has %d additional commits not upstreamed yet.'
3571 % len(extra_commits.splitlines()))
3572 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3573 'before attempting to %s.' % (base_branch, cmd))
3574 return 1
3575
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003576 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003577 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003578 author = None
3579 if options.contributor:
3580 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003581 hook_results = cl.RunHook(
3582 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003583 may_prompt=not options.force,
3584 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003585 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003586 if not hook_results.should_continue():
3587 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003589 # Check the tree status if the tree status URL is set.
3590 status = GetTreeStatus()
3591 if 'closed' == status:
3592 print('The tree is closed. Please wait for it to reopen. Use '
3593 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3594 return 1
3595 elif 'unknown' == status:
3596 print('Unable to determine tree status. Please verify manually and '
3597 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3598 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003599
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003600 change_desc = ChangeDescription(options.message)
3601 if not change_desc.description and cl.GetIssue():
3602 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003603
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003604 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003605 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003606 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003607 else:
3608 print 'No description set.'
3609 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3610 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003611
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003612 # Keep a separate copy for the commit message, because the commit message
3613 # contains the link to the Rietveld issue, while the Rietveld message contains
3614 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003615 # Keep a separate copy for the commit message.
3616 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003617 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003618
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003619 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003620 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003621 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003622 # after it. Add a period on a new line to circumvent this. Also add a space
3623 # before the period to make sure that Gitiles continues to correctly resolve
3624 # the URL.
3625 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003626 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003627 commit_desc.append_footer('Patch from %s.' % options.contributor)
3628
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003629 print('Description:')
3630 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003631
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003632 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003633 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003634 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003635
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003636 # We want to squash all this branch's commits into one commit with the proper
3637 # description. We do this by doing a "reset --soft" to the base branch (which
3638 # keeps the working copy the same), then dcommitting that. If origin/master
3639 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3640 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003641 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003642 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3643 # Delete the branches if they exist.
3644 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3645 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3646 result = RunGitWithCode(showref_cmd)
3647 if result[0] == 0:
3648 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003649
3650 # We might be in a directory that's present in this branch but not in the
3651 # trunk. Move up to the top of the tree so that git commands that expect a
3652 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003653 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003654 if rel_base_path:
3655 os.chdir(rel_base_path)
3656
3657 # Stuff our change into the merge branch.
3658 # We wrap in a try...finally block so if anything goes wrong,
3659 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003660 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003661 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003662 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003663 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003665 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003666 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003667 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003668 RunGit(
3669 [
3670 'commit', '--author', options.contributor,
3671 '-m', commit_desc.description,
3672 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003673 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003674 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003675 if base_has_submodules:
3676 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3677 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3678 RunGit(['checkout', CHERRY_PICK_BRANCH])
3679 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003680 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003681 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003682 mirror = settings.GetGitMirror(remote)
3683 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003684 pending_prefix = settings.GetPendingRefPrefix()
3685 if not pending_prefix or branch.startswith(pending_prefix):
3686 # If not using refs/pending/heads/* at all, or target ref is already set
3687 # to pending, then push to the target ref directly.
3688 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003689 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003690 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003691 else:
3692 # Cherry-pick the change on top of pending ref and then push it.
3693 assert branch.startswith('refs/'), branch
3694 assert pending_prefix[-1] == '/', pending_prefix
3695 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003696 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003697 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003698 if retcode == 0:
3699 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003700 else:
3701 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003702 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003703 'svn', 'dcommit',
3704 '-C%s' % options.similarity,
3705 '--no-rebase', '--rmdir',
3706 ]
3707 if settings.GetForceHttpsCommitUrl():
3708 # Allow forcing https commit URLs for some projects that don't allow
3709 # committing to http URLs (like Google Code).
3710 remote_url = cl.GetGitSvnRemoteUrl()
3711 if urlparse.urlparse(remote_url).scheme == 'http':
3712 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003713 cmd_args.append('--commit-url=%s' % remote_url)
3714 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003715 if 'Committed r' in output:
3716 revision = re.match(
3717 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3718 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003719 finally:
3720 # And then swap back to the original branch and clean up.
3721 RunGit(['checkout', '-q', cl.GetBranch()])
3722 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003723 if base_has_submodules:
3724 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003725
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003726 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003727 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003728 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003729
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003730 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003731 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003732 try:
3733 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3734 # We set pushed_to_pending to False, since it made it all the way to the
3735 # real ref.
3736 pushed_to_pending = False
3737 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003738 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003740 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003741 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003742 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003743 if not to_pending:
3744 if viewvc_url and revision:
3745 change_desc.append_footer(
3746 'Committed: %s%s' % (viewvc_url, revision))
3747 elif revision:
3748 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003749 print ('Closing issue '
3750 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003751 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003752 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003753 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003754 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003755 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003756 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003757 if options.bypass_hooks:
3758 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3759 else:
3760 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003761 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003762 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003763
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003764 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003765 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3766 print 'The commit is in the pending queue (%s).' % pending_ref
3767 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003768 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003769 'footer.' % branch)
3770
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003771 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3772 if os.path.isfile(hook):
3773 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003774
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003775 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003776
3777
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003778def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3779 print
3780 print 'Waiting for commit to be landed on %s...' % real_ref
3781 print '(If you are impatient, you may Ctrl-C once without harm)'
3782 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3783 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003784 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003785
3786 loop = 0
3787 while True:
3788 sys.stdout.write('fetching (%d)... \r' % loop)
3789 sys.stdout.flush()
3790 loop += 1
3791
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003792 if mirror:
3793 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003794 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3795 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3796 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3797 for commit in commits.splitlines():
3798 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3799 print 'Found commit on %s' % real_ref
3800 return commit
3801
3802 current_rev = to_rev
3803
3804
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003805def PushToGitPending(remote, pending_ref, upstream_ref):
3806 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3807
3808 Returns:
3809 (retcode of last operation, output log of last operation).
3810 """
3811 assert pending_ref.startswith('refs/'), pending_ref
3812 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3813 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3814 code = 0
3815 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003816 max_attempts = 3
3817 attempts_left = max_attempts
3818 while attempts_left:
3819 if attempts_left != max_attempts:
3820 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3821 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003822
3823 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003824 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003825 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003826 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003827 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003828 print 'Fetch failed with exit code %d.' % code
3829 if out.strip():
3830 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003831 continue
3832
3833 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003834 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003835 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003836 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003837 if code:
3838 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003839 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3840 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003841 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3842 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003843 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003844 return code, out
3845
3846 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003847 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003848 code, out = RunGitWithCode(
3849 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3850 if code == 0:
3851 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003852 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003853 return code, out
3854
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003855 print 'Push failed with exit code %d.' % code
3856 if out.strip():
3857 print out.strip()
3858 if IsFatalPushFailure(out):
3859 print (
3860 'Fatal push error. Make sure your .netrc credentials and git '
3861 'user.email are correct and you have push access to the repo.')
3862 return code, out
3863
3864 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003865 return code, out
3866
3867
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003868def IsFatalPushFailure(push_stdout):
3869 """True if retrying push won't help."""
3870 return '(prohibited by Gerrit)' in push_stdout
3871
3872
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003873@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003874def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003875 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003877 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003878 # If it looks like previous commits were mirrored with git-svn.
3879 message = """This repository appears to be a git-svn mirror, but no
3880upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3881 else:
3882 message = """This doesn't appear to be an SVN repository.
3883If your project has a true, writeable git repository, you probably want to run
3884'git cl land' instead.
3885If your project has a git mirror of an upstream SVN master, you probably need
3886to run 'git svn init'.
3887
3888Using the wrong command might cause your commit to appear to succeed, and the
3889review to be closed, without actually landing upstream. If you choose to
3890proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003891 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003892 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003893 return SendUpstream(parser, args, 'dcommit')
3894
3895
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003896@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003897def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003898 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003899 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003900 print('This appears to be an SVN repository.')
3901 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003902 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003903 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003904 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905
3906
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003907@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003908def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003909 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003910 parser.add_option('-b', dest='newbranch',
3911 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003912 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003913 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003914 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3915 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003916 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003917 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003918 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003919 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003920 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003921 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003922
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003923
3924 group = optparse.OptionGroup(
3925 parser,
3926 'Options for continuing work on the current issue uploaded from a '
3927 'different clone (e.g. different machine). Must be used independently '
3928 'from the other options. No issue number should be specified, and the '
3929 'branch must have an issue number associated with it')
3930 group.add_option('--reapply', action='store_true', dest='reapply',
3931 help='Reset the branch and reapply the issue.\n'
3932 'CAUTION: This will undo any local changes in this '
3933 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003934
3935 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003936 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003937 parser.add_option_group(group)
3938
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003939 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003940 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003941 auth_config = auth.extract_auth_config_from_options(options)
3942
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003943 cl = Changelist(auth_config=auth_config)
3944
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003945 issue_arg = None
3946 if options.reapply :
3947 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003948 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003949
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003950 issue_arg = cl.GetIssue()
3951 upstream = cl.GetUpstreamBranch()
3952 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003953 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003954
3955 RunGit(['reset', '--hard', upstream])
3956 if options.pull:
3957 RunGit(['pull'])
3958 else:
3959 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003960 parser.error('Must specify issue number or url')
3961 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003962
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003963 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003964 parser.print_help()
3965 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003966
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003967 if cl.IsGerrit():
3968 if options.reject:
3969 parser.error('--reject is not supported with Gerrit codereview.')
3970 if options.nocommit:
3971 parser.error('--nocommit is not supported with Gerrit codereview.')
3972 if options.directory:
3973 parser.error('--directory is not supported with Gerrit codereview.')
3974
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003975 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003976 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003977 return 1
3978
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003979 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003980 if options.reapply:
3981 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003982 if options.force:
3983 RunGit(['branch', '-D', options.newbranch],
3984 stderr=subprocess2.PIPE, error_ok=True)
3985 RunGit(['checkout', '-b', options.newbranch,
3986 Changelist().GetUpstreamBranch()])
3987
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003988 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3989 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003990
3991
3992def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003993 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994 # Provide a wrapper for git svn rebase to help avoid accidental
3995 # git svn dcommit.
3996 # It's the only command that doesn't use parser at all since we just defer
3997 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003998
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003999 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000
4001
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004002def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004003 """Fetches the tree status and returns either 'open', 'closed',
4004 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004005 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006 if url:
4007 status = urllib2.urlopen(url).read().lower()
4008 if status.find('closed') != -1 or status == '0':
4009 return 'closed'
4010 elif status.find('open') != -1 or status == '1':
4011 return 'open'
4012 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004013 return 'unset'
4014
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004015
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004016def GetTreeStatusReason():
4017 """Fetches the tree status from a json url and returns the message
4018 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004019 url = settings.GetTreeStatusUrl()
4020 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004021 connection = urllib2.urlopen(json_url)
4022 status = json.loads(connection.read())
4023 connection.close()
4024 return status['message']
4025
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004026
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004027def GetBuilderMaster(bot_list):
4028 """For a given builder, fetch the master from AE if available."""
4029 map_url = 'https://builders-map.appspot.com/'
4030 try:
4031 master_map = json.load(urllib2.urlopen(map_url))
4032 except urllib2.URLError as e:
4033 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4034 (map_url, e))
4035 except ValueError as e:
4036 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4037 if not master_map:
4038 return None, 'Failed to build master map.'
4039
4040 result_master = ''
4041 for bot in bot_list:
4042 builder = bot.split(':', 1)[0]
4043 master_list = master_map.get(builder, [])
4044 if not master_list:
4045 return None, ('No matching master for builder %s.' % builder)
4046 elif len(master_list) > 1:
4047 return None, ('The builder name %s exists in multiple masters %s.' %
4048 (builder, master_list))
4049 else:
4050 cur_master = master_list[0]
4051 if not result_master:
4052 result_master = cur_master
4053 elif result_master != cur_master:
4054 return None, 'The builders do not belong to the same master.'
4055 return result_master, None
4056
4057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004058def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004059 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004060 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004061 status = GetTreeStatus()
4062 if 'unset' == status:
4063 print 'You must configure your tree status URL by running "git cl config".'
4064 return 2
4065
4066 print "The tree is %s" % status
4067 print
4068 print GetTreeStatusReason()
4069 if status != 'open':
4070 return 1
4071 return 0
4072
4073
maruel@chromium.org15192402012-09-06 12:38:29 +00004074def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004075 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004076 group = optparse.OptionGroup(parser, "Try job options")
4077 group.add_option(
4078 "-b", "--bot", action="append",
4079 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4080 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004081 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004082 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004083 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004084 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004085 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004086 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004087 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004088 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004089 "-r", "--revision",
4090 help="Revision to use for the try job; default: the "
4091 "revision will be determined by the try server; see "
4092 "its waterfall for more info")
4093 group.add_option(
4094 "-c", "--clobber", action="store_true", default=False,
4095 help="Force a clobber before building; e.g. don't do an "
4096 "incremental build")
4097 group.add_option(
4098 "--project",
4099 help="Override which project to use. Projects are defined "
4100 "server-side to define what default bot set to use")
4101 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004102 "-p", "--property", dest="properties", action="append", default=[],
4103 help="Specify generic properties in the form -p key1=value1 -p "
4104 "key2=value2 etc (buildbucket only). The value will be treated as "
4105 "json if decodable, or as string otherwise.")
4106 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004107 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004108 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004109 "--use-rietveld", action="store_true", default=False,
4110 help="Use Rietveld to trigger try jobs.")
4111 group.add_option(
4112 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4113 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004114 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004115 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004116 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004117 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004118
machenbach@chromium.org45453142015-09-15 08:45:22 +00004119 if options.use_rietveld and options.properties:
4120 parser.error('Properties can only be specified with buildbucket')
4121
4122 # Make sure that all properties are prop=value pairs.
4123 bad_params = [x for x in options.properties if '=' not in x]
4124 if bad_params:
4125 parser.error('Got properties with missing "=": %s' % bad_params)
4126
maruel@chromium.org15192402012-09-06 12:38:29 +00004127 if args:
4128 parser.error('Unknown arguments: %s' % args)
4129
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004130 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004131 if not cl.GetIssue():
4132 parser.error('Need to upload first')
4133
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004134 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004135 if props.get('closed'):
4136 parser.error('Cannot send tryjobs for a closed CL')
4137
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004138 if props.get('private'):
4139 parser.error('Cannot use trybots with private issue')
4140
maruel@chromium.org15192402012-09-06 12:38:29 +00004141 if not options.name:
4142 options.name = cl.GetBranch()
4143
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004144 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004145 options.master, err_msg = GetBuilderMaster(options.bot)
4146 if err_msg:
4147 parser.error('Tryserver master cannot be found because: %s\n'
4148 'Please manually specify the tryserver master'
4149 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004150
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004151 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004152 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004153 if not options.bot:
4154 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004155
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004156 # Get try masters from PRESUBMIT.py files.
4157 masters = presubmit_support.DoGetTryMasters(
4158 change,
4159 change.LocalPaths(),
4160 settings.GetRoot(),
4161 None,
4162 None,
4163 options.verbose,
4164 sys.stdout)
4165 if masters:
4166 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004167
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004168 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4169 options.bot = presubmit_support.DoGetTrySlaves(
4170 change,
4171 change.LocalPaths(),
4172 settings.GetRoot(),
4173 None,
4174 None,
4175 options.verbose,
4176 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004177
4178 if not options.bot:
4179 # Get try masters from cq.cfg if any.
4180 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4181 # location.
4182 cq_cfg = os.path.join(change.RepositoryRoot(),
4183 'infra', 'config', 'cq.cfg')
4184 if os.path.exists(cq_cfg):
4185 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004186 cq_masters = commit_queue.get_master_builder_map(
4187 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004188 for master, builders in cq_masters.iteritems():
4189 for builder in builders:
4190 # Skip presubmit builders, because these will fail without LGTM.
4191 if 'presubmit' not in builder.lower():
4192 masters.setdefault(master, {})[builder] = ['defaulttests']
4193 if masters:
4194 return masters
4195
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004196 if not options.bot:
4197 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004198
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004199 builders_and_tests = {}
4200 # TODO(machenbach): The old style command-line options don't support
4201 # multiple try masters yet.
4202 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4203 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4204
4205 for bot in old_style:
4206 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004207 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004208 elif ',' in bot:
4209 parser.error('Specify one bot per --bot flag')
4210 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004211 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004212
4213 for bot, tests in new_style:
4214 builders_and_tests.setdefault(bot, []).extend(tests)
4215
4216 # Return a master map with one master to be backwards compatible. The
4217 # master name defaults to an empty string, which will cause the master
4218 # not to be set on rietveld (deprecated).
4219 return {options.master: builders_and_tests}
4220
4221 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004222
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004223 for builders in masters.itervalues():
4224 if any('triggered' in b for b in builders):
4225 print >> sys.stderr, (
4226 'ERROR You are trying to send a job to a triggered bot. This type of'
4227 ' bot requires an\ninitial job from a parent (usually a builder). '
4228 'Instead send your job to the parent.\n'
4229 'Bot list: %s' % builders)
4230 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004231
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004232 patchset = cl.GetMostRecentPatchset()
4233 if patchset and patchset != cl.GetPatchset():
4234 print(
4235 '\nWARNING Mismatch between local config and server. Did a previous '
4236 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4237 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004238 if options.luci:
4239 trigger_luci_job(cl, masters, options)
4240 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004241 try:
4242 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4243 except BuildbucketResponseException as ex:
4244 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004245 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004246 except Exception as e:
4247 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4248 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4249 e, stacktrace)
4250 return 1
4251 else:
4252 try:
4253 cl.RpcServer().trigger_distributed_try_jobs(
4254 cl.GetIssue(), patchset, options.name, options.clobber,
4255 options.revision, masters)
4256 except urllib2.HTTPError as e:
4257 if e.code == 404:
4258 print('404 from rietveld; '
4259 'did you mean to use "git try" instead of "git cl try"?')
4260 return 1
4261 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004262
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004263 for (master, builders) in sorted(masters.iteritems()):
4264 if master:
4265 print 'Master: %s' % master
4266 length = max(len(builder) for builder in builders)
4267 for builder in sorted(builders):
4268 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004269 return 0
4270
4271
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004272def CMDtry_results(parser, args):
4273 group = optparse.OptionGroup(parser, "Try job results options")
4274 group.add_option(
4275 "-p", "--patchset", type=int, help="patchset number if not current.")
4276 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004277 "--print-master", action='store_true', help="print master name as well.")
4278 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004279 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004280 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004281 group.add_option(
4282 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4283 help="Host of buildbucket. The default host is %default.")
4284 parser.add_option_group(group)
4285 auth.add_auth_options(parser)
4286 options, args = parser.parse_args(args)
4287 if args:
4288 parser.error('Unrecognized args: %s' % ' '.join(args))
4289
4290 auth_config = auth.extract_auth_config_from_options(options)
4291 cl = Changelist(auth_config=auth_config)
4292 if not cl.GetIssue():
4293 parser.error('Need to upload first')
4294
4295 if not options.patchset:
4296 options.patchset = cl.GetMostRecentPatchset()
4297 if options.patchset and options.patchset != cl.GetPatchset():
4298 print(
4299 '\nWARNING Mismatch between local config and server. Did a previous '
4300 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4301 'Continuing using\npatchset %s.\n' % options.patchset)
4302 try:
4303 jobs = fetch_try_jobs(auth_config, cl, options)
4304 except BuildbucketResponseException as ex:
4305 print 'Buildbucket error: %s' % ex
4306 return 1
4307 except Exception as e:
4308 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4309 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4310 e, stacktrace)
4311 return 1
4312 print_tryjobs(options, jobs)
4313 return 0
4314
4315
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004316@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004317def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004318 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004319 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004320 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004321 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004323 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004324 if args:
4325 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004326 branch = cl.GetBranch()
4327 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004328 cl = Changelist()
4329 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004330
4331 # Clear configured merge-base, if there is one.
4332 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004333 else:
4334 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004335 return 0
4336
4337
thestig@chromium.org00858c82013-12-02 23:08:03 +00004338def CMDweb(parser, args):
4339 """Opens the current CL in the web browser."""
4340 _, args = parser.parse_args(args)
4341 if args:
4342 parser.error('Unrecognized args: %s' % ' '.join(args))
4343
4344 issue_url = Changelist().GetIssueURL()
4345 if not issue_url:
4346 print >> sys.stderr, 'ERROR No issue to open'
4347 return 1
4348
4349 webbrowser.open(issue_url)
4350 return 0
4351
4352
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004353def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004354 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004355 auth.add_auth_options(parser)
4356 options, args = parser.parse_args(args)
4357 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004358 if args:
4359 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004360 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004361 props = cl.GetIssueProperties()
4362 if props.get('private'):
4363 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004364 cl.SetFlag('commit', '1')
4365 return 0
4366
4367
groby@chromium.org411034a2013-02-26 15:12:01 +00004368def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004369 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004370 auth.add_auth_options(parser)
4371 options, args = parser.parse_args(args)
4372 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004373 if args:
4374 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004375 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004376 # Ensure there actually is an issue to close.
4377 cl.GetDescription()
4378 cl.CloseIssue()
4379 return 0
4380
4381
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004382def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004383 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004384 auth.add_auth_options(parser)
4385 options, args = parser.parse_args(args)
4386 auth_config = auth.extract_auth_config_from_options(options)
4387 if args:
4388 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004389
4390 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004391 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004392 # Staged changes would be committed along with the patch from last
4393 # upload, hence counted toward the "last upload" side in the final
4394 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004395 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004396 return 1
4397
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004398 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004399 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004400 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004401 if not issue:
4402 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004403 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004404 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004405
4406 # Create a new branch based on the merge-base
4407 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004408 # Clear cached branch in cl object, to avoid overwriting original CL branch
4409 # properties.
4410 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004411 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004412 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004413 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004414 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004415 return rtn
4416
wychen@chromium.org06928532015-02-03 02:11:29 +00004417 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004418 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004419 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004420 finally:
4421 RunGit(['checkout', '-q', branch])
4422 RunGit(['branch', '-D', TMP_BRANCH])
4423
4424 return 0
4425
4426
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004427def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004428 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004429 parser.add_option(
4430 '--no-color',
4431 action='store_true',
4432 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004433 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004434 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004435 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004436
4437 author = RunGit(['config', 'user.email']).strip() or None
4438
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004439 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004440
4441 if args:
4442 if len(args) > 1:
4443 parser.error('Unknown args')
4444 base_branch = args[0]
4445 else:
4446 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004447 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004448
4449 change = cl.GetChange(base_branch, None)
4450 return owners_finder.OwnersFinder(
4451 [f.LocalPath() for f in
4452 cl.GetChange(base_branch, None).AffectedFiles()],
4453 change.RepositoryRoot(), author,
4454 fopen=file, os_path=os.path, glob=glob.glob,
4455 disable_color=options.no_color).run()
4456
4457
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004458def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004459 """Generates a diff command."""
4460 # Generate diff for the current branch's changes.
4461 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4462 upstream_commit, '--' ]
4463
4464 if args:
4465 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004466 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004467 diff_cmd.append(arg)
4468 else:
4469 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004470
4471 return diff_cmd
4472
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004473def MatchingFileType(file_name, extensions):
4474 """Returns true if the file name ends with one of the given extensions."""
4475 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004476
enne@chromium.org555cfe42014-01-29 18:21:39 +00004477@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004478def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004479 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004480 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004481 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004482 parser.add_option('--full', action='store_true',
4483 help='Reformat the full content of all touched files')
4484 parser.add_option('--dry-run', action='store_true',
4485 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004486 parser.add_option('--python', action='store_true',
4487 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004488 parser.add_option('--diff', action='store_true',
4489 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004490 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004491
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004492 # git diff generates paths against the root of the repository. Change
4493 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004494 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004495 if rel_base_path:
4496 os.chdir(rel_base_path)
4497
digit@chromium.org29e47272013-05-17 17:01:46 +00004498 # Grab the merge-base commit, i.e. the upstream commit of the current
4499 # branch when it was created or the last time it was rebased. This is
4500 # to cover the case where the user may have called "git fetch origin",
4501 # moving the origin branch to a newer commit, but hasn't rebased yet.
4502 upstream_commit = None
4503 cl = Changelist()
4504 upstream_branch = cl.GetUpstreamBranch()
4505 if upstream_branch:
4506 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4507 upstream_commit = upstream_commit.strip()
4508
4509 if not upstream_commit:
4510 DieWithError('Could not find base commit for this branch. '
4511 'Are you in detached state?')
4512
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004513 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4514 diff_output = RunGit(changed_files_cmd)
4515 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004516 # Filter out files deleted by this CL
4517 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004518
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004519 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4520 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4521 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004522 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004523
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004524 top_dir = os.path.normpath(
4525 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4526
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004527 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4528 # formatted. This is used to block during the presubmit.
4529 return_value = 0
4530
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004531 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004532 # Locate the clang-format binary in the checkout
4533 try:
4534 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4535 except clang_format.NotFoundError, e:
4536 DieWithError(e)
4537
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004538 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004539 cmd = [clang_format_tool]
4540 if not opts.dry_run and not opts.diff:
4541 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004542 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004543 if opts.diff:
4544 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004545 else:
4546 env = os.environ.copy()
4547 env['PATH'] = str(os.path.dirname(clang_format_tool))
4548 try:
4549 script = clang_format.FindClangFormatScriptInChromiumTree(
4550 'clang-format-diff.py')
4551 except clang_format.NotFoundError, e:
4552 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004553
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004554 cmd = [sys.executable, script, '-p0']
4555 if not opts.dry_run and not opts.diff:
4556 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004557
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004558 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4559 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004560
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004561 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4562 if opts.diff:
4563 sys.stdout.write(stdout)
4564 if opts.dry_run and len(stdout) > 0:
4565 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004566
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004567 # Similar code to above, but using yapf on .py files rather than clang-format
4568 # on C/C++ files
4569 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004570 yapf_tool = gclient_utils.FindExecutable('yapf')
4571 if yapf_tool is None:
4572 DieWithError('yapf not found in PATH')
4573
4574 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004575 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004576 cmd = [yapf_tool]
4577 if not opts.dry_run and not opts.diff:
4578 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004579 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004580 if opts.diff:
4581 sys.stdout.write(stdout)
4582 else:
4583 # TODO(sbc): yapf --lines mode still has some issues.
4584 # https://github.com/google/yapf/issues/154
4585 DieWithError('--python currently only works with --full')
4586
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004587 # Dart's formatter does not have the nice property of only operating on
4588 # modified chunks, so hard code full.
4589 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004590 try:
4591 command = [dart_format.FindDartFmtToolInChromiumTree()]
4592 if not opts.dry_run and not opts.diff:
4593 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004594 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004595
ppi@chromium.org6593d932016-03-03 15:41:15 +00004596 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004597 if opts.dry_run and stdout:
4598 return_value = 2
4599 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004600 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4601 'found in this checkout. Files in other languages are still ' +
4602 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004603
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004604 # Format GN build files. Always run on full build files for canonical form.
4605 if gn_diff_files:
4606 cmd = ['gn', 'format']
4607 if not opts.dry_run and not opts.diff:
4608 cmd.append('--in-place')
4609 for gn_diff_file in gn_diff_files:
4610 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4611 if opts.diff:
4612 sys.stdout.write(stdout)
4613
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004614 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004615
4616
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004617@subcommand.usage('<codereview url or issue id>')
4618def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004619 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004620 _, args = parser.parse_args(args)
4621
4622 if len(args) != 1:
4623 parser.print_help()
4624 return 1
4625
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004626 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004627 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004628 parser.print_help()
4629 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004630 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004631
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004632 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004633 output = RunGit(['config', '--local', '--get-regexp',
4634 r'branch\..*\.%s' % issueprefix],
4635 error_ok=True)
4636 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004637 if issue == target_issue:
4638 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004639
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004640 branches = []
4641 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004642 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004643 if len(branches) == 0:
4644 print 'No branch found for issue %s.' % target_issue
4645 return 1
4646 if len(branches) == 1:
4647 RunGit(['checkout', branches[0]])
4648 else:
4649 print 'Multiple branches match issue %s:' % target_issue
4650 for i in range(len(branches)):
4651 print '%d: %s' % (i, branches[i])
4652 which = raw_input('Choose by index: ')
4653 try:
4654 RunGit(['checkout', branches[int(which)]])
4655 except (IndexError, ValueError):
4656 print 'Invalid selection, not checking out any branch.'
4657 return 1
4658
4659 return 0
4660
4661
maruel@chromium.org29404b52014-09-08 22:58:00 +00004662def CMDlol(parser, args):
4663 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004664 print zlib.decompress(base64.b64decode(
4665 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4666 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4667 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4668 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004669 return 0
4670
4671
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004672class OptionParser(optparse.OptionParser):
4673 """Creates the option parse and add --verbose support."""
4674 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004675 optparse.OptionParser.__init__(
4676 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004677 self.add_option(
4678 '-v', '--verbose', action='count', default=0,
4679 help='Use 2 times for more debugging info')
4680
4681 def parse_args(self, args=None, values=None):
4682 options, args = optparse.OptionParser.parse_args(self, args, values)
4683 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4684 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4685 return options, args
4686
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004687
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004688def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004689 if sys.hexversion < 0x02060000:
4690 print >> sys.stderr, (
4691 '\nYour python version %s is unsupported, please upgrade.\n' %
4692 sys.version.split(' ', 1)[0])
4693 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004694
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004695 # Reload settings.
4696 global settings
4697 settings = Settings()
4698
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004699 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004700 dispatcher = subcommand.CommandDispatcher(__name__)
4701 try:
4702 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004703 except auth.AuthenticationError as e:
4704 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004705 except urllib2.HTTPError, e:
4706 if e.code != 500:
4707 raise
4708 DieWithError(
4709 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4710 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004711 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004712
4713
4714if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004715 # These affect sys.stdout so do it outside of main() to simplify mocks in
4716 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004717 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004718 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004719 try:
4720 sys.exit(main(sys.argv[1:]))
4721 except KeyboardInterrupt:
4722 sys.stderr.write('interrupted\n')
4723 sys.exit(1)