blob: 6ee0cf29cb9a20a3bfee8893f2e23b04706226c1 [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 cc = self.GetCCList().split(',')
2396 if options.cc:
2397 cc.extend(options.cc)
2398 cc = filter(None, cc)
2399 if cc:
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002400 refspec_opts.extend('cc=' + email.strip() for email in cc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002401
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002402 if change_desc.get_reviewers():
2403 refspec_opts.extend('r=' + email.strip()
2404 for email in change_desc.get_reviewers())
2405
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002406
2407 refspec_suffix = ''
2408 if refspec_opts:
2409 refspec_suffix = '%' + ','.join(refspec_opts)
2410 assert ' ' not in refspec_suffix, (
2411 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002412 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002413
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002414 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002415 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002416 print_stdout=True,
2417 # Flush after every line: useful for seeing progress when running as
2418 # recipe.
2419 filter_fn=lambda _: sys.stdout.flush())
2420
2421 if options.squash:
2422 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2423 change_numbers = [m.group(1)
2424 for m in map(regex.match, push_stdout.splitlines())
2425 if m]
2426 if len(change_numbers) != 1:
2427 DieWithError(
2428 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2429 'Change-Id: %s') % (len(change_numbers), change_id))
2430 self.SetIssue(change_numbers[0])
2431 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2432 ref_to_push])
2433 return 0
2434
2435
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002436
2437_CODEREVIEW_IMPLEMENTATIONS = {
2438 'rietveld': _RietveldChangelistImpl,
2439 'gerrit': _GerritChangelistImpl,
2440}
2441
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002442
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002443class ChangeDescription(object):
2444 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002445 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002446 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002447
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002448 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002449 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002450
agable@chromium.org42c20792013-09-12 17:34:49 +00002451 @property # www.logilab.org/ticket/89786
2452 def description(self): # pylint: disable=E0202
2453 return '\n'.join(self._description_lines)
2454
2455 def set_description(self, desc):
2456 if isinstance(desc, basestring):
2457 lines = desc.splitlines()
2458 else:
2459 lines = [line.rstrip() for line in desc]
2460 while lines and not lines[0]:
2461 lines.pop(0)
2462 while lines and not lines[-1]:
2463 lines.pop(-1)
2464 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002465
piman@chromium.org336f9122014-09-04 02:16:55 +00002466 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002467 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002468 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002469 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002470 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002471 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002472
agable@chromium.org42c20792013-09-12 17:34:49 +00002473 # Get the set of R= and TBR= lines and remove them from the desciption.
2474 regexp = re.compile(self.R_LINE)
2475 matches = [regexp.match(line) for line in self._description_lines]
2476 new_desc = [l for i, l in enumerate(self._description_lines)
2477 if not matches[i]]
2478 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002479
agable@chromium.org42c20792013-09-12 17:34:49 +00002480 # Construct new unified R= and TBR= lines.
2481 r_names = []
2482 tbr_names = []
2483 for match in matches:
2484 if not match:
2485 continue
2486 people = cleanup_list([match.group(2).strip()])
2487 if match.group(1) == 'TBR':
2488 tbr_names.extend(people)
2489 else:
2490 r_names.extend(people)
2491 for name in r_names:
2492 if name not in reviewers:
2493 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002494 if add_owners_tbr:
2495 owners_db = owners.Database(change.RepositoryRoot(),
2496 fopen=file, os_path=os.path, glob=glob.glob)
2497 all_reviewers = set(tbr_names + reviewers)
2498 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2499 all_reviewers)
2500 tbr_names.extend(owners_db.reviewers_for(missing_files,
2501 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002502 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2503 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2504
2505 # Put the new lines in the description where the old first R= line was.
2506 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2507 if 0 <= line_loc < len(self._description_lines):
2508 if new_tbr_line:
2509 self._description_lines.insert(line_loc, new_tbr_line)
2510 if new_r_line:
2511 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002512 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002513 if new_r_line:
2514 self.append_footer(new_r_line)
2515 if new_tbr_line:
2516 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002517
2518 def prompt(self):
2519 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002520 self.set_description([
2521 '# Enter a description of the change.',
2522 '# This will be displayed on the codereview site.',
2523 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002524 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002525 '--------------------',
2526 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002527
agable@chromium.org42c20792013-09-12 17:34:49 +00002528 regexp = re.compile(self.BUG_LINE)
2529 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002530 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002531 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002532 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002533 if not content:
2534 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002535 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002536
2537 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002538 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2539 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002540 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002541 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002542
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002543 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002544 if self._description_lines:
2545 # Add an empty line if either the last line or the new line isn't a tag.
2546 last_line = self._description_lines[-1]
2547 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2548 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2549 self._description_lines.append('')
2550 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002551
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002552 def get_reviewers(self):
2553 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002554 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2555 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002556 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002557
2558
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002559def get_approving_reviewers(props):
2560 """Retrieves the reviewers that approved a CL from the issue properties with
2561 messages.
2562
2563 Note that the list may contain reviewers that are not committer, thus are not
2564 considered by the CQ.
2565 """
2566 return sorted(
2567 set(
2568 message['sender']
2569 for message in props['messages']
2570 if message['approval'] and message['sender'] in props['reviewers']
2571 )
2572 )
2573
2574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575def FindCodereviewSettingsFile(filename='codereview.settings'):
2576 """Finds the given file starting in the cwd and going up.
2577
2578 Only looks up to the top of the repository unless an
2579 'inherit-review-settings-ok' file exists in the root of the repository.
2580 """
2581 inherit_ok_file = 'inherit-review-settings-ok'
2582 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002583 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002584 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2585 root = '/'
2586 while True:
2587 if filename in os.listdir(cwd):
2588 if os.path.isfile(os.path.join(cwd, filename)):
2589 return open(os.path.join(cwd, filename))
2590 if cwd == root:
2591 break
2592 cwd = os.path.dirname(cwd)
2593
2594
2595def LoadCodereviewSettingsFromFile(fileobj):
2596 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002597 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002599 def SetProperty(name, setting, unset_error_ok=False):
2600 fullname = 'rietveld.' + name
2601 if setting in keyvals:
2602 RunGit(['config', fullname, keyvals[setting]])
2603 else:
2604 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2605
2606 SetProperty('server', 'CODE_REVIEW_SERVER')
2607 # Only server setting is required. Other settings can be absent.
2608 # In that case, we ignore errors raised during option deletion attempt.
2609 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002610 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002611 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2612 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002613 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002614 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002615 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2616 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002617 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002618 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002619 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002620 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2621 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002623 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002624 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002625
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002626 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2627 RunGit(['config', 'gerrit.squash-uploads',
2628 keyvals['GERRIT_SQUASH_UPLOADS']])
2629
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002630 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2631 #should be of the form
2632 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2633 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2634 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2635 keyvals['ORIGIN_URL_CONFIG']])
2636
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002637
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002638def urlretrieve(source, destination):
2639 """urllib is broken for SSL connections via a proxy therefore we
2640 can't use urllib.urlretrieve()."""
2641 with open(destination, 'w') as f:
2642 f.write(urllib2.urlopen(source).read())
2643
2644
ukai@chromium.org712d6102013-11-27 00:52:58 +00002645def hasSheBang(fname):
2646 """Checks fname is a #! script."""
2647 with open(fname) as f:
2648 return f.read(2).startswith('#!')
2649
2650
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002651# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2652def DownloadHooks(*args, **kwargs):
2653 pass
2654
2655
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002656def DownloadGerritHook(force):
2657 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002658
2659 Args:
2660 force: True to update hooks. False to install hooks if not present.
2661 """
2662 if not settings.GetIsGerrit():
2663 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002664 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002665 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2666 if not os.access(dst, os.X_OK):
2667 if os.path.exists(dst):
2668 if not force:
2669 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002670 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002671 print(
2672 'WARNING: installing Gerrit commit-msg hook.\n'
2673 ' This behavior of git cl will soon be disabled.\n'
2674 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002675 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002676 if not hasSheBang(dst):
2677 DieWithError('Not a script: %s\n'
2678 'You need to download from\n%s\n'
2679 'into .git/hooks/commit-msg and '
2680 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002681 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2682 except Exception:
2683 if os.path.exists(dst):
2684 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002685 DieWithError('\nFailed to download hooks.\n'
2686 'You need to download from\n%s\n'
2687 'into .git/hooks/commit-msg and '
2688 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002689
2690
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002691
2692def GetRietveldCodereviewSettingsInteractively():
2693 """Prompt the user for settings."""
2694 server = settings.GetDefaultServerUrl(error_ok=True)
2695 prompt = 'Rietveld server (host[:port])'
2696 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2697 newserver = ask_for_data(prompt + ':')
2698 if not server and not newserver:
2699 newserver = DEFAULT_SERVER
2700 if newserver:
2701 newserver = gclient_utils.UpgradeToHttps(newserver)
2702 if newserver != server:
2703 RunGit(['config', 'rietveld.server', newserver])
2704
2705 def SetProperty(initial, caption, name, is_url):
2706 prompt = caption
2707 if initial:
2708 prompt += ' ("x" to clear) [%s]' % initial
2709 new_val = ask_for_data(prompt + ':')
2710 if new_val == 'x':
2711 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2712 elif new_val:
2713 if is_url:
2714 new_val = gclient_utils.UpgradeToHttps(new_val)
2715 if new_val != initial:
2716 RunGit(['config', 'rietveld.' + name, new_val])
2717
2718 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2719 SetProperty(settings.GetDefaultPrivateFlag(),
2720 'Private flag (rietveld only)', 'private', False)
2721 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2722 'tree-status-url', False)
2723 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2724 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2725 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2726 'run-post-upload-hook', False)
2727
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002728@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002730 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002732 print('WARNING: git cl config works for Rietveld only.\n'
2733 'For Gerrit, see http://crbug.com/579160.')
2734 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002735 parser.add_option('--activate-update', action='store_true',
2736 help='activate auto-updating [rietveld] section in '
2737 '.git/config')
2738 parser.add_option('--deactivate-update', action='store_true',
2739 help='deactivate auto-updating [rietveld] section in '
2740 '.git/config')
2741 options, args = parser.parse_args(args)
2742
2743 if options.deactivate_update:
2744 RunGit(['config', 'rietveld.autoupdate', 'false'])
2745 return
2746
2747 if options.activate_update:
2748 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2749 return
2750
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002751 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002752 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002753 return 0
2754
2755 url = args[0]
2756 if not url.endswith('codereview.settings'):
2757 url = os.path.join(url, 'codereview.settings')
2758
2759 # Load code review settings and download hooks (if available).
2760 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2761 return 0
2762
2763
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002764def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002765 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002766 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2767 branch = ShortBranchName(branchref)
2768 _, args = parser.parse_args(args)
2769 if not args:
2770 print("Current base-url:")
2771 return RunGit(['config', 'branch.%s.base-url' % branch],
2772 error_ok=False).strip()
2773 else:
2774 print("Setting base-url to %s" % args[0])
2775 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2776 error_ok=False).strip()
2777
2778
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002779def color_for_status(status):
2780 """Maps a Changelist status to color, for CMDstatus and other tools."""
2781 return {
2782 'unsent': Fore.RED,
2783 'waiting': Fore.BLUE,
2784 'reply': Fore.YELLOW,
2785 'lgtm': Fore.GREEN,
2786 'commit': Fore.MAGENTA,
2787 'closed': Fore.CYAN,
2788 'error': Fore.WHITE,
2789 }.get(status, Fore.WHITE)
2790
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002791def fetch_cl_status(branch, auth_config=None):
2792 """Fetches information for an issue and returns (branch, issue, status)."""
2793 cl = Changelist(branchref=branch, auth_config=auth_config)
2794 url = cl.GetIssueURL()
2795 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002796
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002797 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002798 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002799 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002800
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002801 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002802
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002803def get_cl_statuses(
2804 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002805 """Returns a blocking iterable of (branch, issue, color) for given branches.
2806
2807 If fine_grained is true, this will fetch CL statuses from the server.
2808 Otherwise, simply indicate if there's a matching url for the given branches.
2809
2810 If max_processes is specified, it is used as the maximum number of processes
2811 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2812 spawned.
2813 """
2814 # Silence upload.py otherwise it becomes unwieldly.
2815 upload.verbosity = 0
2816
2817 if fine_grained:
2818 # Process one branch synchronously to work through authentication, then
2819 # spawn processes to process all the other branches in parallel.
2820 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002821 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2822 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002823
2824 branches_to_fetch = branches[1:]
2825 pool = ThreadPool(
2826 min(max_processes, len(branches_to_fetch))
2827 if max_processes is not None
2828 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002829 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002830 yield x
2831 else:
2832 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2833 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002834 cl = Changelist(branchref=b, auth_config=auth_config)
2835 url = cl.GetIssueURL()
2836 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002837
rmistry@google.com2dd99862015-06-22 12:22:18 +00002838
2839def upload_branch_deps(cl, args):
2840 """Uploads CLs of local branches that are dependents of the current branch.
2841
2842 If the local branch dependency tree looks like:
2843 test1 -> test2.1 -> test3.1
2844 -> test3.2
2845 -> test2.2 -> test3.3
2846
2847 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2848 run on the dependent branches in this order:
2849 test2.1, test3.1, test3.2, test2.2, test3.3
2850
2851 Note: This function does not rebase your local dependent branches. Use it when
2852 you make a change to the parent branch that will not conflict with its
2853 dependent branches, and you would like their dependencies updated in
2854 Rietveld.
2855 """
2856 if git_common.is_dirty_git_tree('upload-branch-deps'):
2857 return 1
2858
2859 root_branch = cl.GetBranch()
2860 if root_branch is None:
2861 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2862 'Get on a branch!')
2863 if not cl.GetIssue() or not cl.GetPatchset():
2864 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2865 'patchset dependencies without an uploaded CL.')
2866
2867 branches = RunGit(['for-each-ref',
2868 '--format=%(refname:short) %(upstream:short)',
2869 'refs/heads'])
2870 if not branches:
2871 print('No local branches found.')
2872 return 0
2873
2874 # Create a dictionary of all local branches to the branches that are dependent
2875 # on it.
2876 tracked_to_dependents = collections.defaultdict(list)
2877 for b in branches.splitlines():
2878 tokens = b.split()
2879 if len(tokens) == 2:
2880 branch_name, tracked = tokens
2881 tracked_to_dependents[tracked].append(branch_name)
2882
2883 print
2884 print 'The dependent local branches of %s are:' % root_branch
2885 dependents = []
2886 def traverse_dependents_preorder(branch, padding=''):
2887 dependents_to_process = tracked_to_dependents.get(branch, [])
2888 padding += ' '
2889 for dependent in dependents_to_process:
2890 print '%s%s' % (padding, dependent)
2891 dependents.append(dependent)
2892 traverse_dependents_preorder(dependent, padding)
2893 traverse_dependents_preorder(root_branch)
2894 print
2895
2896 if not dependents:
2897 print 'There are no dependent local branches for %s' % root_branch
2898 return 0
2899
2900 print ('This command will checkout all dependent branches and run '
2901 '"git cl upload".')
2902 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2903
andybons@chromium.org962f9462016-02-03 20:00:42 +00002904 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002905 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002906 args.extend(['-t', 'Updated patchset dependency'])
2907
rmistry@google.com2dd99862015-06-22 12:22:18 +00002908 # Record all dependents that failed to upload.
2909 failures = {}
2910 # Go through all dependents, checkout the branch and upload.
2911 try:
2912 for dependent_branch in dependents:
2913 print
2914 print '--------------------------------------'
2915 print 'Running "git cl upload" from %s:' % dependent_branch
2916 RunGit(['checkout', '-q', dependent_branch])
2917 print
2918 try:
2919 if CMDupload(OptionParser(), args) != 0:
2920 print 'Upload failed for %s!' % dependent_branch
2921 failures[dependent_branch] = 1
2922 except: # pylint: disable=W0702
2923 failures[dependent_branch] = 1
2924 print
2925 finally:
2926 # Swap back to the original root branch.
2927 RunGit(['checkout', '-q', root_branch])
2928
2929 print
2930 print 'Upload complete for dependent branches!'
2931 for dependent_branch in dependents:
2932 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2933 print ' %s : %s' % (dependent_branch, upload_status)
2934 print
2935
2936 return 0
2937
2938
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002939def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002940 """Show status of changelists.
2941
2942 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002943 - Red not sent for review or broken
2944 - Blue waiting for review
2945 - Yellow waiting for you to reply to review
2946 - Green LGTM'ed
2947 - Magenta in the commit queue
2948 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002949
2950 Also see 'git cl comments'.
2951 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002952 parser.add_option('--field',
2953 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002954 parser.add_option('-f', '--fast', action='store_true',
2955 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002956 parser.add_option(
2957 '-j', '--maxjobs', action='store', type=int,
2958 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002959
2960 auth.add_auth_options(parser)
2961 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002962 if args:
2963 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002964 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002966 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002967 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002968 if options.field.startswith('desc'):
2969 print cl.GetDescription()
2970 elif options.field == 'id':
2971 issueid = cl.GetIssue()
2972 if issueid:
2973 print issueid
2974 elif options.field == 'patch':
2975 patchset = cl.GetPatchset()
2976 if patchset:
2977 print patchset
2978 elif options.field == 'url':
2979 url = cl.GetIssueURL()
2980 if url:
2981 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002982 return 0
2983
2984 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2985 if not branches:
2986 print('No local branch found.')
2987 return 0
2988
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002989 changes = (
2990 Changelist(branchref=b, auth_config=auth_config)
2991 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002992 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002993 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002994 alignment = max(5, max(len(b) for b in branches))
2995 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002996 output = get_cl_statuses(branches,
2997 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002998 max_processes=options.maxjobs,
2999 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003000
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003001 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003002 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003003 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003004 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003005 b, i, status = output.next()
3006 branch_statuses[b] = (i, status)
3007 issue_url, status = branch_statuses.pop(branch)
3008 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003009 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003010 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003011 color = ''
3012 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003013 status_str = '(%s)' % status if status else ''
3014 print ' %*s : %s%s %s%s' % (
3015 alignment, ShortBranchName(branch), color, issue_url, status_str,
3016 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003017
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003018 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003019 print
3020 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003021 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003022 if not cl.GetIssue():
3023 print 'No issue assigned.'
3024 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003025 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003026 if not options.fast:
3027 print 'Issue description:'
3028 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003029 return 0
3030
3031
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003032def colorize_CMDstatus_doc():
3033 """To be called once in main() to add colors to git cl status help."""
3034 colors = [i for i in dir(Fore) if i[0].isupper()]
3035
3036 def colorize_line(line):
3037 for color in colors:
3038 if color in line.upper():
3039 # Extract whitespaces first and the leading '-'.
3040 indent = len(line) - len(line.lstrip(' ')) + 1
3041 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3042 return line
3043
3044 lines = CMDstatus.__doc__.splitlines()
3045 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3046
3047
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003048@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003049def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003050 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003051
3052 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003053 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003054 parser.add_option('-r', '--reverse', action='store_true',
3055 help='Lookup the branch(es) for the specified issues. If '
3056 'no issues are specified, all branches with mapped '
3057 'issues will be listed.')
3058 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059
dnj@chromium.org406c4402015-03-03 17:22:28 +00003060 if options.reverse:
3061 branches = RunGit(['for-each-ref', 'refs/heads',
3062 '--format=%(refname:short)']).splitlines()
3063
3064 # Reverse issue lookup.
3065 issue_branch_map = {}
3066 for branch in branches:
3067 cl = Changelist(branchref=branch)
3068 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3069 if not args:
3070 args = sorted(issue_branch_map.iterkeys())
3071 for issue in args:
3072 if not issue:
3073 continue
3074 print 'Branch for issue number %s: %s' % (
3075 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3076 else:
3077 cl = Changelist()
3078 if len(args) > 0:
3079 try:
3080 issue = int(args[0])
3081 except ValueError:
3082 DieWithError('Pass a number to set the issue or none to list it.\n'
3083 'Maybe you want to run git cl status?')
3084 cl.SetIssue(issue)
3085 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003086 return 0
3087
3088
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003089def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003090 """Shows or posts review comments for any changelist."""
3091 parser.add_option('-a', '--add-comment', dest='comment',
3092 help='comment to add to an issue')
3093 parser.add_option('-i', dest='issue',
3094 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003095 parser.add_option('-j', '--json-file',
3096 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003097 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003098 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003099 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003100
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003101 issue = None
3102 if options.issue:
3103 try:
3104 issue = int(options.issue)
3105 except ValueError:
3106 DieWithError('A review issue id is expected to be a number')
3107
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003108 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003109
3110 if options.comment:
3111 cl.AddComment(options.comment)
3112 return 0
3113
3114 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003115 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003116 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003117 summary.append({
3118 'date': message['date'],
3119 'lgtm': False,
3120 'message': message['text'],
3121 'not_lgtm': False,
3122 'sender': message['sender'],
3123 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003124 if message['disapproval']:
3125 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003126 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003127 elif message['approval']:
3128 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003129 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003130 elif message['sender'] == data['owner_email']:
3131 color = Fore.MAGENTA
3132 else:
3133 color = Fore.BLUE
3134 print '\n%s%s %s%s' % (
3135 color, message['date'].split('.', 1)[0], message['sender'],
3136 Fore.RESET)
3137 if message['text'].strip():
3138 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003139 if options.json_file:
3140 with open(options.json_file, 'wb') as f:
3141 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003142 return 0
3143
3144
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003145def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003146 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003147 parser.add_option('-d', '--display', action='store_true',
3148 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003149 auth.add_auth_options(parser)
3150 options, _ = parser.parse_args(args)
3151 auth_config = auth.extract_auth_config_from_options(options)
3152 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003153 if not cl.GetIssue():
3154 DieWithError('This branch has no associated changelist.')
3155 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003156 if options.display:
3157 print description.description
3158 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003159 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003160 if cl.GetDescription() != description.description:
3161 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003162 return 0
3163
3164
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003165def CreateDescriptionFromLog(args):
3166 """Pulls out the commit log to use as a base for the CL description."""
3167 log_args = []
3168 if len(args) == 1 and not args[0].endswith('.'):
3169 log_args = [args[0] + '..']
3170 elif len(args) == 1 and args[0].endswith('...'):
3171 log_args = [args[0][:-1]]
3172 elif len(args) == 2:
3173 log_args = [args[0] + '..' + args[1]]
3174 else:
3175 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003176 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003177
3178
thestig@chromium.org44202a22014-03-11 19:22:18 +00003179def CMDlint(parser, args):
3180 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003181 parser.add_option('--filter', action='append', metavar='-x,+y',
3182 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003183 auth.add_auth_options(parser)
3184 options, args = parser.parse_args(args)
3185 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003186
3187 # Access to a protected member _XX of a client class
3188 # pylint: disable=W0212
3189 try:
3190 import cpplint
3191 import cpplint_chromium
3192 except ImportError:
3193 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3194 return 1
3195
3196 # Change the current working directory before calling lint so that it
3197 # shows the correct base.
3198 previous_cwd = os.getcwd()
3199 os.chdir(settings.GetRoot())
3200 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003201 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003202 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3203 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003204 if not files:
3205 print "Cannot lint an empty CL"
3206 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003207
3208 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003209 command = args + files
3210 if options.filter:
3211 command = ['--filter=' + ','.join(options.filter)] + command
3212 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003213
3214 white_regex = re.compile(settings.GetLintRegex())
3215 black_regex = re.compile(settings.GetLintIgnoreRegex())
3216 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3217 for filename in filenames:
3218 if white_regex.match(filename):
3219 if black_regex.match(filename):
3220 print "Ignoring file %s" % filename
3221 else:
3222 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3223 extra_check_functions)
3224 else:
3225 print "Skipping file %s" % filename
3226 finally:
3227 os.chdir(previous_cwd)
3228 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3229 if cpplint._cpplint_state.error_count != 0:
3230 return 1
3231 return 0
3232
3233
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003234def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003235 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003236 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003237 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003238 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003239 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003240 auth.add_auth_options(parser)
3241 options, args = parser.parse_args(args)
3242 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003243
sbc@chromium.org71437c02015-04-09 19:29:40 +00003244 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003245 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246 return 1
3247
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003248 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003249 if args:
3250 base_branch = args[0]
3251 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003252 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003253 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003254
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003255 cl.RunHook(
3256 committing=not options.upload,
3257 may_prompt=False,
3258 verbose=options.verbose,
3259 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003260 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003261
3262
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003263def AddChangeIdToCommitMessage(options, args):
3264 """Re-commits using the current message, assumes the commit hook is in
3265 place.
3266 """
3267 log_desc = options.message or CreateDescriptionFromLog(args)
3268 git_command = ['commit', '--amend', '-m', log_desc]
3269 RunGit(git_command)
3270 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003271 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003272 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00003273 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003274 else:
3275 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
3276
3277
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003278def GenerateGerritChangeId(message):
3279 """Returns Ixxxxxx...xxx change id.
3280
3281 Works the same way as
3282 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3283 but can be called on demand on all platforms.
3284
3285 The basic idea is to generate git hash of a state of the tree, original commit
3286 message, author/committer info and timestamps.
3287 """
3288 lines = []
3289 tree_hash = RunGitSilent(['write-tree'])
3290 lines.append('tree %s' % tree_hash.strip())
3291 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3292 if code == 0:
3293 lines.append('parent %s' % parent.strip())
3294 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3295 lines.append('author %s' % author.strip())
3296 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3297 lines.append('committer %s' % committer.strip())
3298 lines.append('')
3299 # Note: Gerrit's commit-hook actually cleans message of some lines and
3300 # whitespace. This code is not doing this, but it clearly won't decrease
3301 # entropy.
3302 lines.append(message)
3303 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3304 stdin='\n'.join(lines))
3305 return 'I%s' % change_hash.strip()
3306
3307
wittman@chromium.org455dc922015-01-26 20:15:50 +00003308def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3309 """Computes the remote branch ref to use for the CL.
3310
3311 Args:
3312 remote (str): The git remote for the CL.
3313 remote_branch (str): The git remote branch for the CL.
3314 target_branch (str): The target branch specified by the user.
3315 pending_prefix (str): The pending prefix from the settings.
3316 """
3317 if not (remote and remote_branch):
3318 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003319
wittman@chromium.org455dc922015-01-26 20:15:50 +00003320 if target_branch:
3321 # Cannonicalize branch references to the equivalent local full symbolic
3322 # refs, which are then translated into the remote full symbolic refs
3323 # below.
3324 if '/' not in target_branch:
3325 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3326 else:
3327 prefix_replacements = (
3328 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3329 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3330 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3331 )
3332 match = None
3333 for regex, replacement in prefix_replacements:
3334 match = re.search(regex, target_branch)
3335 if match:
3336 remote_branch = target_branch.replace(match.group(0), replacement)
3337 break
3338 if not match:
3339 # This is a branch path but not one we recognize; use as-is.
3340 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003341 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3342 # Handle the refs that need to land in different refs.
3343 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003344
wittman@chromium.org455dc922015-01-26 20:15:50 +00003345 # Create the true path to the remote branch.
3346 # Does the following translation:
3347 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3348 # * refs/remotes/origin/master -> refs/heads/master
3349 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3350 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3351 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3352 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3353 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3354 'refs/heads/')
3355 elif remote_branch.startswith('refs/remotes/branch-heads'):
3356 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3357 # If a pending prefix exists then replace refs/ with it.
3358 if pending_prefix:
3359 remote_branch = remote_branch.replace('refs/', pending_prefix)
3360 return remote_branch
3361
3362
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003363def cleanup_list(l):
3364 """Fixes a list so that comma separated items are put as individual items.
3365
3366 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3367 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3368 """
3369 items = sum((i.split(',') for i in l), [])
3370 stripped_items = (i.strip() for i in items)
3371 return sorted(filter(None, stripped_items))
3372
3373
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003374@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003375def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003376 """Uploads the current changelist to codereview.
3377
3378 Can skip dependency patchset uploads for a branch by running:
3379 git config branch.branch_name.skip-deps-uploads True
3380 To unset run:
3381 git config --unset branch.branch_name.skip-deps-uploads
3382 Can also set the above globally by using the --global flag.
3383 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003384 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3385 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003386 parser.add_option('--bypass-watchlists', action='store_true',
3387 dest='bypass_watchlists',
3388 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003389 parser.add_option('-f', action='store_true', dest='force',
3390 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003391 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003392 parser.add_option('-t', dest='title',
3393 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003394 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003395 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003396 help='reviewer email addresses')
3397 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003398 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003399 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003400 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003401 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003402 parser.add_option('--emulate_svn_auto_props',
3403 '--emulate-svn-auto-props',
3404 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003405 dest="emulate_svn_auto_props",
3406 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003407 parser.add_option('-c', '--use-commit-queue', action='store_true',
3408 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003409 parser.add_option('--private', action='store_true',
3410 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003411 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003412 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003413 metavar='TARGET',
3414 help='Apply CL to remote ref TARGET. ' +
3415 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003416 parser.add_option('--squash', action='store_true',
3417 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003418 parser.add_option('--no-squash', action='store_true',
3419 help='Don\'t squash multiple commits into one ' +
3420 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003421 parser.add_option('--email', default=None,
3422 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003423 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3424 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003425 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3426 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003427 help='Send the patchset to do a CQ dry run right after '
3428 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003429 parser.add_option('--dependencies', action='store_true',
3430 help='Uploads CLs of all the local branches that depend on '
3431 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003432
rmistry@google.com2dd99862015-06-22 12:22:18 +00003433 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003434 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003435 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003436 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003437 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003438
sbc@chromium.org71437c02015-04-09 19:29:40 +00003439 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003440 return 1
3441
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003442 options.reviewers = cleanup_list(options.reviewers)
3443 options.cc = cleanup_list(options.cc)
3444
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003445 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3446 settings.GetIsGerrit()
3447
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003448 cl = Changelist(auth_config=auth_config)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003449 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003450
3451
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003452def IsSubmoduleMergeCommit(ref):
3453 # When submodules are added to the repo, we expect there to be a single
3454 # non-git-svn merge commit at remote HEAD with a signature comment.
3455 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003456 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003457 return RunGit(cmd) != ''
3458
3459
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003460def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003461 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003462
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003463 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3464 upstream and closes the issue automatically and atomically.
3465
3466 Otherwise (in case of Rietveld):
3467 Squashes branch into a single commit.
3468 Updates changelog with metadata (e.g. pointer to review).
3469 Pushes/dcommits the code upstream.
3470 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003471 """
3472 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3473 help='bypass upload presubmit hook')
3474 parser.add_option('-m', dest='message',
3475 help="override review description")
3476 parser.add_option('-f', action='store_true', dest='force',
3477 help="force yes to questions (don't prompt)")
3478 parser.add_option('-c', dest='contributor',
3479 help="external contributor for patch (appended to " +
3480 "description and used as author for git). Should be " +
3481 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003482 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003483 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003484 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003485 auth_config = auth.extract_auth_config_from_options(options)
3486
3487 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003488
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003489 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3490 if cl.IsGerrit():
3491 if options.message:
3492 # This could be implemented, but it requires sending a new patch to
3493 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3494 # Besides, Gerrit has the ability to change the commit message on submit
3495 # automatically, thus there is no need to support this option (so far?).
3496 parser.error('-m MESSAGE option is not supported for Gerrit.')
3497 if options.contributor:
3498 parser.error(
3499 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3500 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3501 'the contributor\'s "name <email>". If you can\'t upload such a '
3502 'commit for review, contact your repository admin and request'
3503 '"Forge-Author" permission.')
3504 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3505 options.verbose)
3506
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003507 current = cl.GetBranch()
3508 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3509 if not settings.GetIsGitSvn() and remote == '.':
3510 print
3511 print 'Attempting to push branch %r into another local branch!' % current
3512 print
3513 print 'Either reparent this branch on top of origin/master:'
3514 print ' git reparent-branch --root'
3515 print
3516 print 'OR run `git rebase-update` if you think the parent branch is already'
3517 print 'committed.'
3518 print
3519 print ' Current parent: %r' % upstream_branch
3520 return 1
3521
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003522 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523 # Default to merging against our best guess of the upstream branch.
3524 args = [cl.GetUpstreamBranch()]
3525
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003526 if options.contributor:
3527 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3528 print "Please provide contibutor as 'First Last <email@example.com>'"
3529 return 1
3530
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003531 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003532 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533
sbc@chromium.org71437c02015-04-09 19:29:40 +00003534 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003535 return 1
3536
3537 # This rev-list syntax means "show all commits not in my branch that
3538 # are in base_branch".
3539 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3540 base_branch]).splitlines()
3541 if upstream_commits:
3542 print ('Base branch "%s" has %d commits '
3543 'not in this branch.' % (base_branch, len(upstream_commits)))
3544 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3545 return 1
3546
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003547 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003548 svn_head = None
3549 if cmd == 'dcommit' or base_has_submodules:
3550 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3551 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003552
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003553 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003554 # If the base_head is a submodule merge commit, the first parent of the
3555 # base_head should be a git-svn commit, which is what we're interested in.
3556 base_svn_head = base_branch
3557 if base_has_submodules:
3558 base_svn_head += '^1'
3559
3560 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561 if extra_commits:
3562 print ('This branch has %d additional commits not upstreamed yet.'
3563 % len(extra_commits.splitlines()))
3564 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3565 'before attempting to %s.' % (base_branch, cmd))
3566 return 1
3567
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003568 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003569 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003570 author = None
3571 if options.contributor:
3572 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003573 hook_results = cl.RunHook(
3574 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003575 may_prompt=not options.force,
3576 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003577 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003578 if not hook_results.should_continue():
3579 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003580
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003581 # Check the tree status if the tree status URL is set.
3582 status = GetTreeStatus()
3583 if 'closed' == status:
3584 print('The tree is closed. Please wait for it to reopen. Use '
3585 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3586 return 1
3587 elif 'unknown' == status:
3588 print('Unable to determine tree status. Please verify manually and '
3589 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3590 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003591
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003592 change_desc = ChangeDescription(options.message)
3593 if not change_desc.description and cl.GetIssue():
3594 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003595
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003596 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003597 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003598 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003599 else:
3600 print 'No description set.'
3601 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3602 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003603
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003604 # Keep a separate copy for the commit message, because the commit message
3605 # contains the link to the Rietveld issue, while the Rietveld message contains
3606 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003607 # Keep a separate copy for the commit message.
3608 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003609 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003610
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003611 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003612 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003613 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003614 # after it. Add a period on a new line to circumvent this. Also add a space
3615 # before the period to make sure that Gitiles continues to correctly resolve
3616 # the URL.
3617 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003618 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003619 commit_desc.append_footer('Patch from %s.' % options.contributor)
3620
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003621 print('Description:')
3622 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003623
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003624 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003625 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003626 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003627
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003628 # We want to squash all this branch's commits into one commit with the proper
3629 # description. We do this by doing a "reset --soft" to the base branch (which
3630 # keeps the working copy the same), then dcommitting that. If origin/master
3631 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3632 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003633 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003634 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3635 # Delete the branches if they exist.
3636 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3637 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3638 result = RunGitWithCode(showref_cmd)
3639 if result[0] == 0:
3640 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003641
3642 # We might be in a directory that's present in this branch but not in the
3643 # trunk. Move up to the top of the tree so that git commands that expect a
3644 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003645 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003646 if rel_base_path:
3647 os.chdir(rel_base_path)
3648
3649 # Stuff our change into the merge branch.
3650 # We wrap in a try...finally block so if anything goes wrong,
3651 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003652 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003653 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003654 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003655 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003656 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003657 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003658 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003659 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003660 RunGit(
3661 [
3662 'commit', '--author', options.contributor,
3663 '-m', commit_desc.description,
3664 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003666 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003667 if base_has_submodules:
3668 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3669 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3670 RunGit(['checkout', CHERRY_PICK_BRANCH])
3671 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003672 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003673 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003674 mirror = settings.GetGitMirror(remote)
3675 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003676 pending_prefix = settings.GetPendingRefPrefix()
3677 if not pending_prefix or branch.startswith(pending_prefix):
3678 # If not using refs/pending/heads/* at all, or target ref is already set
3679 # to pending, then push to the target ref directly.
3680 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003681 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003682 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003683 else:
3684 # Cherry-pick the change on top of pending ref and then push it.
3685 assert branch.startswith('refs/'), branch
3686 assert pending_prefix[-1] == '/', pending_prefix
3687 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003688 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003689 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003690 if retcode == 0:
3691 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003692 else:
3693 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003694 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003695 'svn', 'dcommit',
3696 '-C%s' % options.similarity,
3697 '--no-rebase', '--rmdir',
3698 ]
3699 if settings.GetForceHttpsCommitUrl():
3700 # Allow forcing https commit URLs for some projects that don't allow
3701 # committing to http URLs (like Google Code).
3702 remote_url = cl.GetGitSvnRemoteUrl()
3703 if urlparse.urlparse(remote_url).scheme == 'http':
3704 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003705 cmd_args.append('--commit-url=%s' % remote_url)
3706 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003707 if 'Committed r' in output:
3708 revision = re.match(
3709 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3710 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003711 finally:
3712 # And then swap back to the original branch and clean up.
3713 RunGit(['checkout', '-q', cl.GetBranch()])
3714 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003715 if base_has_submodules:
3716 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003717
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003718 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003719 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003720 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003721
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003722 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003723 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003724 try:
3725 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3726 # We set pushed_to_pending to False, since it made it all the way to the
3727 # real ref.
3728 pushed_to_pending = False
3729 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003730 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003731
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003732 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003733 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003734 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003735 if not to_pending:
3736 if viewvc_url and revision:
3737 change_desc.append_footer(
3738 'Committed: %s%s' % (viewvc_url, revision))
3739 elif revision:
3740 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003741 print ('Closing issue '
3742 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003743 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003744 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003745 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003746 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003747 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003748 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003749 if options.bypass_hooks:
3750 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3751 else:
3752 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003753 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003754 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003755
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003756 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003757 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3758 print 'The commit is in the pending queue (%s).' % pending_ref
3759 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003760 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003761 'footer.' % branch)
3762
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003763 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3764 if os.path.isfile(hook):
3765 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003766
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003767 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003768
3769
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003770def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3771 print
3772 print 'Waiting for commit to be landed on %s...' % real_ref
3773 print '(If you are impatient, you may Ctrl-C once without harm)'
3774 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3775 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003776 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003777
3778 loop = 0
3779 while True:
3780 sys.stdout.write('fetching (%d)... \r' % loop)
3781 sys.stdout.flush()
3782 loop += 1
3783
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003784 if mirror:
3785 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003786 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3787 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3788 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3789 for commit in commits.splitlines():
3790 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3791 print 'Found commit on %s' % real_ref
3792 return commit
3793
3794 current_rev = to_rev
3795
3796
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003797def PushToGitPending(remote, pending_ref, upstream_ref):
3798 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3799
3800 Returns:
3801 (retcode of last operation, output log of last operation).
3802 """
3803 assert pending_ref.startswith('refs/'), pending_ref
3804 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3805 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3806 code = 0
3807 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003808 max_attempts = 3
3809 attempts_left = max_attempts
3810 while attempts_left:
3811 if attempts_left != max_attempts:
3812 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3813 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003814
3815 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003816 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003817 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003818 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003819 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003820 print 'Fetch failed with exit code %d.' % code
3821 if out.strip():
3822 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003823 continue
3824
3825 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003826 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003827 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003828 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003829 if code:
3830 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003831 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3832 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003833 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3834 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003835 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003836 return code, out
3837
3838 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003839 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003840 code, out = RunGitWithCode(
3841 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3842 if code == 0:
3843 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003844 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003845 return code, out
3846
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003847 print 'Push failed with exit code %d.' % code
3848 if out.strip():
3849 print out.strip()
3850 if IsFatalPushFailure(out):
3851 print (
3852 'Fatal push error. Make sure your .netrc credentials and git '
3853 'user.email are correct and you have push access to the repo.')
3854 return code, out
3855
3856 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003857 return code, out
3858
3859
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003860def IsFatalPushFailure(push_stdout):
3861 """True if retrying push won't help."""
3862 return '(prohibited by Gerrit)' in push_stdout
3863
3864
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003865@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003866def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003867 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003868 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003869 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003870 # If it looks like previous commits were mirrored with git-svn.
3871 message = """This repository appears to be a git-svn mirror, but no
3872upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3873 else:
3874 message = """This doesn't appear to be an SVN repository.
3875If your project has a true, writeable git repository, you probably want to run
3876'git cl land' instead.
3877If your project has a git mirror of an upstream SVN master, you probably need
3878to run 'git svn init'.
3879
3880Using the wrong command might cause your commit to appear to succeed, and the
3881review to be closed, without actually landing upstream. If you choose to
3882proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003883 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003884 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003885 return SendUpstream(parser, args, 'dcommit')
3886
3887
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003888@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003889def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003890 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003891 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892 print('This appears to be an SVN repository.')
3893 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003894 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003895 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003896 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003897
3898
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003899@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003900def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003901 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003902 parser.add_option('-b', dest='newbranch',
3903 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003904 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003905 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003906 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3907 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003908 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003909 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003910 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003911 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003912 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003913 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003914
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003915
3916 group = optparse.OptionGroup(
3917 parser,
3918 'Options for continuing work on the current issue uploaded from a '
3919 'different clone (e.g. different machine). Must be used independently '
3920 'from the other options. No issue number should be specified, and the '
3921 'branch must have an issue number associated with it')
3922 group.add_option('--reapply', action='store_true', dest='reapply',
3923 help='Reset the branch and reapply the issue.\n'
3924 'CAUTION: This will undo any local changes in this '
3925 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003926
3927 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003928 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003929 parser.add_option_group(group)
3930
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003931 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003933 auth_config = auth.extract_auth_config_from_options(options)
3934
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003935 cl = Changelist(auth_config=auth_config)
3936
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003937 issue_arg = None
3938 if options.reapply :
3939 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003940 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003941
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003942 issue_arg = cl.GetIssue()
3943 upstream = cl.GetUpstreamBranch()
3944 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003945 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003946
3947 RunGit(['reset', '--hard', upstream])
3948 if options.pull:
3949 RunGit(['pull'])
3950 else:
3951 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003952 parser.error('Must specify issue number or url')
3953 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003954
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003955 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003956 parser.print_help()
3957 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003958
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003959 if cl.IsGerrit():
3960 if options.reject:
3961 parser.error('--reject is not supported with Gerrit codereview.')
3962 if options.nocommit:
3963 parser.error('--nocommit is not supported with Gerrit codereview.')
3964 if options.directory:
3965 parser.error('--directory is not supported with Gerrit codereview.')
3966
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003967 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003968 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003969 return 1
3970
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003971 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003972 if options.reapply:
3973 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003974 if options.force:
3975 RunGit(['branch', '-D', options.newbranch],
3976 stderr=subprocess2.PIPE, error_ok=True)
3977 RunGit(['checkout', '-b', options.newbranch,
3978 Changelist().GetUpstreamBranch()])
3979
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003980 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3981 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003982
3983
3984def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003985 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986 # Provide a wrapper for git svn rebase to help avoid accidental
3987 # git svn dcommit.
3988 # It's the only command that doesn't use parser at all since we just defer
3989 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003990
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003991 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003992
3993
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003994def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995 """Fetches the tree status and returns either 'open', 'closed',
3996 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003997 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998 if url:
3999 status = urllib2.urlopen(url).read().lower()
4000 if status.find('closed') != -1 or status == '0':
4001 return 'closed'
4002 elif status.find('open') != -1 or status == '1':
4003 return 'open'
4004 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005 return 'unset'
4006
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004008def GetTreeStatusReason():
4009 """Fetches the tree status from a json url and returns the message
4010 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004011 url = settings.GetTreeStatusUrl()
4012 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004013 connection = urllib2.urlopen(json_url)
4014 status = json.loads(connection.read())
4015 connection.close()
4016 return status['message']
4017
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004018
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004019def GetBuilderMaster(bot_list):
4020 """For a given builder, fetch the master from AE if available."""
4021 map_url = 'https://builders-map.appspot.com/'
4022 try:
4023 master_map = json.load(urllib2.urlopen(map_url))
4024 except urllib2.URLError as e:
4025 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4026 (map_url, e))
4027 except ValueError as e:
4028 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4029 if not master_map:
4030 return None, 'Failed to build master map.'
4031
4032 result_master = ''
4033 for bot in bot_list:
4034 builder = bot.split(':', 1)[0]
4035 master_list = master_map.get(builder, [])
4036 if not master_list:
4037 return None, ('No matching master for builder %s.' % builder)
4038 elif len(master_list) > 1:
4039 return None, ('The builder name %s exists in multiple masters %s.' %
4040 (builder, master_list))
4041 else:
4042 cur_master = master_list[0]
4043 if not result_master:
4044 result_master = cur_master
4045 elif result_master != cur_master:
4046 return None, 'The builders do not belong to the same master.'
4047 return result_master, None
4048
4049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004050def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004051 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004052 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053 status = GetTreeStatus()
4054 if 'unset' == status:
4055 print 'You must configure your tree status URL by running "git cl config".'
4056 return 2
4057
4058 print "The tree is %s" % status
4059 print
4060 print GetTreeStatusReason()
4061 if status != 'open':
4062 return 1
4063 return 0
4064
4065
maruel@chromium.org15192402012-09-06 12:38:29 +00004066def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004067 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004068 group = optparse.OptionGroup(parser, "Try job options")
4069 group.add_option(
4070 "-b", "--bot", action="append",
4071 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4072 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004073 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004074 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004075 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004076 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004077 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004078 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004079 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004080 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004081 "-r", "--revision",
4082 help="Revision to use for the try job; default: the "
4083 "revision will be determined by the try server; see "
4084 "its waterfall for more info")
4085 group.add_option(
4086 "-c", "--clobber", action="store_true", default=False,
4087 help="Force a clobber before building; e.g. don't do an "
4088 "incremental build")
4089 group.add_option(
4090 "--project",
4091 help="Override which project to use. Projects are defined "
4092 "server-side to define what default bot set to use")
4093 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004094 "-p", "--property", dest="properties", action="append", default=[],
4095 help="Specify generic properties in the form -p key1=value1 -p "
4096 "key2=value2 etc (buildbucket only). The value will be treated as "
4097 "json if decodable, or as string otherwise.")
4098 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004099 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004100 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004101 "--use-rietveld", action="store_true", default=False,
4102 help="Use Rietveld to trigger try jobs.")
4103 group.add_option(
4104 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4105 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004106 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004107 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004108 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004109 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004110
machenbach@chromium.org45453142015-09-15 08:45:22 +00004111 if options.use_rietveld and options.properties:
4112 parser.error('Properties can only be specified with buildbucket')
4113
4114 # Make sure that all properties are prop=value pairs.
4115 bad_params = [x for x in options.properties if '=' not in x]
4116 if bad_params:
4117 parser.error('Got properties with missing "=": %s' % bad_params)
4118
maruel@chromium.org15192402012-09-06 12:38:29 +00004119 if args:
4120 parser.error('Unknown arguments: %s' % args)
4121
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004122 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004123 if not cl.GetIssue():
4124 parser.error('Need to upload first')
4125
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004126 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004127 if props.get('closed'):
4128 parser.error('Cannot send tryjobs for a closed CL')
4129
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004130 if props.get('private'):
4131 parser.error('Cannot use trybots with private issue')
4132
maruel@chromium.org15192402012-09-06 12:38:29 +00004133 if not options.name:
4134 options.name = cl.GetBranch()
4135
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004136 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004137 options.master, err_msg = GetBuilderMaster(options.bot)
4138 if err_msg:
4139 parser.error('Tryserver master cannot be found because: %s\n'
4140 'Please manually specify the tryserver master'
4141 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004142
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004143 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004144 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004145 if not options.bot:
4146 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004147
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004148 # Get try masters from PRESUBMIT.py files.
4149 masters = presubmit_support.DoGetTryMasters(
4150 change,
4151 change.LocalPaths(),
4152 settings.GetRoot(),
4153 None,
4154 None,
4155 options.verbose,
4156 sys.stdout)
4157 if masters:
4158 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004159
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004160 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4161 options.bot = presubmit_support.DoGetTrySlaves(
4162 change,
4163 change.LocalPaths(),
4164 settings.GetRoot(),
4165 None,
4166 None,
4167 options.verbose,
4168 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004169
4170 if not options.bot:
4171 # Get try masters from cq.cfg if any.
4172 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4173 # location.
4174 cq_cfg = os.path.join(change.RepositoryRoot(),
4175 'infra', 'config', 'cq.cfg')
4176 if os.path.exists(cq_cfg):
4177 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004178 cq_masters = commit_queue.get_master_builder_map(
4179 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004180 for master, builders in cq_masters.iteritems():
4181 for builder in builders:
4182 # Skip presubmit builders, because these will fail without LGTM.
4183 if 'presubmit' not in builder.lower():
4184 masters.setdefault(master, {})[builder] = ['defaulttests']
4185 if masters:
4186 return masters
4187
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004188 if not options.bot:
4189 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004190
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004191 builders_and_tests = {}
4192 # TODO(machenbach): The old style command-line options don't support
4193 # multiple try masters yet.
4194 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4195 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4196
4197 for bot in old_style:
4198 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004199 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004200 elif ',' in bot:
4201 parser.error('Specify one bot per --bot flag')
4202 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004203 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004204
4205 for bot, tests in new_style:
4206 builders_and_tests.setdefault(bot, []).extend(tests)
4207
4208 # Return a master map with one master to be backwards compatible. The
4209 # master name defaults to an empty string, which will cause the master
4210 # not to be set on rietveld (deprecated).
4211 return {options.master: builders_and_tests}
4212
4213 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004214
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004215 for builders in masters.itervalues():
4216 if any('triggered' in b for b in builders):
4217 print >> sys.stderr, (
4218 'ERROR You are trying to send a job to a triggered bot. This type of'
4219 ' bot requires an\ninitial job from a parent (usually a builder). '
4220 'Instead send your job to the parent.\n'
4221 'Bot list: %s' % builders)
4222 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004223
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004224 patchset = cl.GetMostRecentPatchset()
4225 if patchset and patchset != cl.GetPatchset():
4226 print(
4227 '\nWARNING Mismatch between local config and server. Did a previous '
4228 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4229 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004230 if options.luci:
4231 trigger_luci_job(cl, masters, options)
4232 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004233 try:
4234 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4235 except BuildbucketResponseException as ex:
4236 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004237 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004238 except Exception as e:
4239 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4240 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4241 e, stacktrace)
4242 return 1
4243 else:
4244 try:
4245 cl.RpcServer().trigger_distributed_try_jobs(
4246 cl.GetIssue(), patchset, options.name, options.clobber,
4247 options.revision, masters)
4248 except urllib2.HTTPError as e:
4249 if e.code == 404:
4250 print('404 from rietveld; '
4251 'did you mean to use "git try" instead of "git cl try"?')
4252 return 1
4253 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004254
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004255 for (master, builders) in sorted(masters.iteritems()):
4256 if master:
4257 print 'Master: %s' % master
4258 length = max(len(builder) for builder in builders)
4259 for builder in sorted(builders):
4260 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004261 return 0
4262
4263
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004264def CMDtry_results(parser, args):
4265 group = optparse.OptionGroup(parser, "Try job results options")
4266 group.add_option(
4267 "-p", "--patchset", type=int, help="patchset number if not current.")
4268 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004269 "--print-master", action='store_true', help="print master name as well.")
4270 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004271 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004272 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004273 group.add_option(
4274 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4275 help="Host of buildbucket. The default host is %default.")
4276 parser.add_option_group(group)
4277 auth.add_auth_options(parser)
4278 options, args = parser.parse_args(args)
4279 if args:
4280 parser.error('Unrecognized args: %s' % ' '.join(args))
4281
4282 auth_config = auth.extract_auth_config_from_options(options)
4283 cl = Changelist(auth_config=auth_config)
4284 if not cl.GetIssue():
4285 parser.error('Need to upload first')
4286
4287 if not options.patchset:
4288 options.patchset = cl.GetMostRecentPatchset()
4289 if options.patchset and options.patchset != cl.GetPatchset():
4290 print(
4291 '\nWARNING Mismatch between local config and server. Did a previous '
4292 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4293 'Continuing using\npatchset %s.\n' % options.patchset)
4294 try:
4295 jobs = fetch_try_jobs(auth_config, cl, options)
4296 except BuildbucketResponseException as ex:
4297 print 'Buildbucket error: %s' % ex
4298 return 1
4299 except Exception as e:
4300 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4301 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4302 e, stacktrace)
4303 return 1
4304 print_tryjobs(options, jobs)
4305 return 0
4306
4307
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004308@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004309def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004310 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004311 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004312 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004313 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004315 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004316 if args:
4317 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004318 branch = cl.GetBranch()
4319 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004320 cl = Changelist()
4321 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004322
4323 # Clear configured merge-base, if there is one.
4324 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004325 else:
4326 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004327 return 0
4328
4329
thestig@chromium.org00858c82013-12-02 23:08:03 +00004330def CMDweb(parser, args):
4331 """Opens the current CL in the web browser."""
4332 _, args = parser.parse_args(args)
4333 if args:
4334 parser.error('Unrecognized args: %s' % ' '.join(args))
4335
4336 issue_url = Changelist().GetIssueURL()
4337 if not issue_url:
4338 print >> sys.stderr, 'ERROR No issue to open'
4339 return 1
4340
4341 webbrowser.open(issue_url)
4342 return 0
4343
4344
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004345def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004346 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004347 auth.add_auth_options(parser)
4348 options, args = parser.parse_args(args)
4349 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004350 if args:
4351 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004352 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004353 props = cl.GetIssueProperties()
4354 if props.get('private'):
4355 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004356 cl.SetFlag('commit', '1')
4357 return 0
4358
4359
groby@chromium.org411034a2013-02-26 15:12:01 +00004360def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004361 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004362 auth.add_auth_options(parser)
4363 options, args = parser.parse_args(args)
4364 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004365 if args:
4366 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004367 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004368 # Ensure there actually is an issue to close.
4369 cl.GetDescription()
4370 cl.CloseIssue()
4371 return 0
4372
4373
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004374def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004375 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004376 auth.add_auth_options(parser)
4377 options, args = parser.parse_args(args)
4378 auth_config = auth.extract_auth_config_from_options(options)
4379 if args:
4380 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004381
4382 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004383 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004384 # Staged changes would be committed along with the patch from last
4385 # upload, hence counted toward the "last upload" side in the final
4386 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004387 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004388 return 1
4389
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004390 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004391 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004392 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004393 if not issue:
4394 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004395 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004396 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004397
4398 # Create a new branch based on the merge-base
4399 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004400 # Clear cached branch in cl object, to avoid overwriting original CL branch
4401 # properties.
4402 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004403 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004404 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004405 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004406 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004407 return rtn
4408
wychen@chromium.org06928532015-02-03 02:11:29 +00004409 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004410 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004411 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004412 finally:
4413 RunGit(['checkout', '-q', branch])
4414 RunGit(['branch', '-D', TMP_BRANCH])
4415
4416 return 0
4417
4418
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004419def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004420 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004421 parser.add_option(
4422 '--no-color',
4423 action='store_true',
4424 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004425 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004426 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004427 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004428
4429 author = RunGit(['config', 'user.email']).strip() or None
4430
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004431 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004432
4433 if args:
4434 if len(args) > 1:
4435 parser.error('Unknown args')
4436 base_branch = args[0]
4437 else:
4438 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004439 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004440
4441 change = cl.GetChange(base_branch, None)
4442 return owners_finder.OwnersFinder(
4443 [f.LocalPath() for f in
4444 cl.GetChange(base_branch, None).AffectedFiles()],
4445 change.RepositoryRoot(), author,
4446 fopen=file, os_path=os.path, glob=glob.glob,
4447 disable_color=options.no_color).run()
4448
4449
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004450def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004451 """Generates a diff command."""
4452 # Generate diff for the current branch's changes.
4453 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4454 upstream_commit, '--' ]
4455
4456 if args:
4457 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004458 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004459 diff_cmd.append(arg)
4460 else:
4461 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004462
4463 return diff_cmd
4464
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004465def MatchingFileType(file_name, extensions):
4466 """Returns true if the file name ends with one of the given extensions."""
4467 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004468
enne@chromium.org555cfe42014-01-29 18:21:39 +00004469@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004470def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004471 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004472 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004473 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004474 parser.add_option('--full', action='store_true',
4475 help='Reformat the full content of all touched files')
4476 parser.add_option('--dry-run', action='store_true',
4477 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004478 parser.add_option('--python', action='store_true',
4479 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004480 parser.add_option('--diff', action='store_true',
4481 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004482 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004483
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004484 # git diff generates paths against the root of the repository. Change
4485 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004486 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004487 if rel_base_path:
4488 os.chdir(rel_base_path)
4489
digit@chromium.org29e47272013-05-17 17:01:46 +00004490 # Grab the merge-base commit, i.e. the upstream commit of the current
4491 # branch when it was created or the last time it was rebased. This is
4492 # to cover the case where the user may have called "git fetch origin",
4493 # moving the origin branch to a newer commit, but hasn't rebased yet.
4494 upstream_commit = None
4495 cl = Changelist()
4496 upstream_branch = cl.GetUpstreamBranch()
4497 if upstream_branch:
4498 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4499 upstream_commit = upstream_commit.strip()
4500
4501 if not upstream_commit:
4502 DieWithError('Could not find base commit for this branch. '
4503 'Are you in detached state?')
4504
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004505 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4506 diff_output = RunGit(changed_files_cmd)
4507 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004508 # Filter out files deleted by this CL
4509 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004510
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004511 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4512 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4513 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004514 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004515
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004516 top_dir = os.path.normpath(
4517 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4518
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004519 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4520 # formatted. This is used to block during the presubmit.
4521 return_value = 0
4522
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004523 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004524 # Locate the clang-format binary in the checkout
4525 try:
4526 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4527 except clang_format.NotFoundError, e:
4528 DieWithError(e)
4529
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004530 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004531 cmd = [clang_format_tool]
4532 if not opts.dry_run and not opts.diff:
4533 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004534 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004535 if opts.diff:
4536 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004537 else:
4538 env = os.environ.copy()
4539 env['PATH'] = str(os.path.dirname(clang_format_tool))
4540 try:
4541 script = clang_format.FindClangFormatScriptInChromiumTree(
4542 'clang-format-diff.py')
4543 except clang_format.NotFoundError, e:
4544 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004545
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004546 cmd = [sys.executable, script, '-p0']
4547 if not opts.dry_run and not opts.diff:
4548 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004549
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004550 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4551 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004552
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004553 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4554 if opts.diff:
4555 sys.stdout.write(stdout)
4556 if opts.dry_run and len(stdout) > 0:
4557 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004558
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004559 # Similar code to above, but using yapf on .py files rather than clang-format
4560 # on C/C++ files
4561 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004562 yapf_tool = gclient_utils.FindExecutable('yapf')
4563 if yapf_tool is None:
4564 DieWithError('yapf not found in PATH')
4565
4566 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004567 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004568 cmd = [yapf_tool]
4569 if not opts.dry_run and not opts.diff:
4570 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004571 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004572 if opts.diff:
4573 sys.stdout.write(stdout)
4574 else:
4575 # TODO(sbc): yapf --lines mode still has some issues.
4576 # https://github.com/google/yapf/issues/154
4577 DieWithError('--python currently only works with --full')
4578
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004579 # Dart's formatter does not have the nice property of only operating on
4580 # modified chunks, so hard code full.
4581 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004582 try:
4583 command = [dart_format.FindDartFmtToolInChromiumTree()]
4584 if not opts.dry_run and not opts.diff:
4585 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004586 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004587
ppi@chromium.org6593d932016-03-03 15:41:15 +00004588 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004589 if opts.dry_run and stdout:
4590 return_value = 2
4591 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004592 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4593 'found in this checkout. Files in other languages are still ' +
4594 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004595
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004596 # Format GN build files. Always run on full build files for canonical form.
4597 if gn_diff_files:
4598 cmd = ['gn', 'format']
4599 if not opts.dry_run and not opts.diff:
4600 cmd.append('--in-place')
4601 for gn_diff_file in gn_diff_files:
4602 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4603 if opts.diff:
4604 sys.stdout.write(stdout)
4605
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004606 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004607
4608
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004609@subcommand.usage('<codereview url or issue id>')
4610def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004611 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004612 _, args = parser.parse_args(args)
4613
4614 if len(args) != 1:
4615 parser.print_help()
4616 return 1
4617
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004618 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004619 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004620 parser.print_help()
4621 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004622 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004623
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004624 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004625 output = RunGit(['config', '--local', '--get-regexp',
4626 r'branch\..*\.%s' % issueprefix],
4627 error_ok=True)
4628 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004629 if issue == target_issue:
4630 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004631
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004632 branches = []
4633 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004634 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004635 if len(branches) == 0:
4636 print 'No branch found for issue %s.' % target_issue
4637 return 1
4638 if len(branches) == 1:
4639 RunGit(['checkout', branches[0]])
4640 else:
4641 print 'Multiple branches match issue %s:' % target_issue
4642 for i in range(len(branches)):
4643 print '%d: %s' % (i, branches[i])
4644 which = raw_input('Choose by index: ')
4645 try:
4646 RunGit(['checkout', branches[int(which)]])
4647 except (IndexError, ValueError):
4648 print 'Invalid selection, not checking out any branch.'
4649 return 1
4650
4651 return 0
4652
4653
maruel@chromium.org29404b52014-09-08 22:58:00 +00004654def CMDlol(parser, args):
4655 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004656 print zlib.decompress(base64.b64decode(
4657 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4658 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4659 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4660 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004661 return 0
4662
4663
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004664class OptionParser(optparse.OptionParser):
4665 """Creates the option parse and add --verbose support."""
4666 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004667 optparse.OptionParser.__init__(
4668 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004669 self.add_option(
4670 '-v', '--verbose', action='count', default=0,
4671 help='Use 2 times for more debugging info')
4672
4673 def parse_args(self, args=None, values=None):
4674 options, args = optparse.OptionParser.parse_args(self, args, values)
4675 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4676 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4677 return options, args
4678
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004679
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004680def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004681 if sys.hexversion < 0x02060000:
4682 print >> sys.stderr, (
4683 '\nYour python version %s is unsupported, please upgrade.\n' %
4684 sys.version.split(' ', 1)[0])
4685 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004686
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004687 # Reload settings.
4688 global settings
4689 settings = Settings()
4690
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004691 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004692 dispatcher = subcommand.CommandDispatcher(__name__)
4693 try:
4694 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004695 except auth.AuthenticationError as e:
4696 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004697 except urllib2.HTTPError, e:
4698 if e.code != 500:
4699 raise
4700 DieWithError(
4701 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4702 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004703 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004704
4705
4706if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004707 # These affect sys.stdout so do it outside of main() to simplify mocks in
4708 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004709 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004710 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004711 try:
4712 sys.exit(main(sys.argv[1:]))
4713 except KeyboardInterrupt:
4714 sys.stderr.write('interrupted\n')
4715 sys.exit(1)