blob: b1121560ee3b1197e155e0a575891558347dc116 [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
2238 if options.title:
2239 # TODO(tandrii): it's now supported by Gerrit, implement!
2240 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
2241 return 1
2242
2243 if options.squash:
2244 if not self.GetIssue():
2245 # TODO(tandrii): deperecate this after 2016Q2. Backwards compatibility
2246 # with shadow branch, which used to contain change-id for a given
2247 # branch, using which we can fetch actual issue number and set it as the
2248 # property of the branch, which is the new way.
2249 message = RunGitSilent([
2250 'show', '--format=%B', '-s',
2251 'refs/heads/git_cl_uploads/%s' % self.GetBranch()])
2252 if message:
2253 change_ids = git_footers.get_footer_change_id(message.strip())
2254 if change_ids and len(change_ids) == 1:
2255 details = self._GetChangeDetail(issue=change_ids[0])
2256 if details:
2257 print('WARNING: found old upload in branch git_cl_uploads/%s '
2258 'corresponding to issue %s' %
2259 (self.GetBranch(), details['_number']))
2260 self.SetIssue(details['_number'])
2261 if not self.GetIssue():
2262 DieWithError(
2263 '\n' # For readability of the blob below.
2264 'Found old upload in branch git_cl_uploads/%s, '
2265 'but failed to find corresponding Gerrit issue.\n'
2266 'If you know the issue number, set it manually first:\n'
2267 ' git cl issue 123456\n'
2268 'If you intended to upload this CL as new issue, '
2269 'just delete or rename the old upload branch:\n'
2270 ' git rename-branch git_cl_uploads/%s old_upload-%s\n'
2271 'After that, please run git cl upload again.' %
2272 tuple([self.GetBranch()] * 3))
2273 # End of backwards compatability.
2274
2275 if self.GetIssue():
2276 # Try to get the message from a previous upload.
2277 message = self.GetDescription()
2278 if not message:
2279 DieWithError(
2280 'failed to fetch description from current Gerrit issue %d\n'
2281 '%s' % (self.GetIssue(), self.GetIssueURL()))
2282 change_id = self._GetChangeDetail()['change_id']
2283 while True:
2284 footer_change_ids = git_footers.get_footer_change_id(message)
2285 if footer_change_ids == [change_id]:
2286 break
2287 if not footer_change_ids:
2288 message = git_footers.add_footer_change_id(message, change_id)
2289 print('WARNING: appended missing Change-Id to issue description')
2290 continue
2291 # There is already a valid footer but with different or several ids.
2292 # Doing this automatically is non-trivial as we don't want to lose
2293 # existing other footers, yet we want to append just 1 desired
2294 # Change-Id. Thus, just create a new footer, but let user verify the
2295 # new description.
2296 message = '%s\n\nChange-Id: %s' % (message, change_id)
2297 print(
2298 'WARNING: issue %s has Change-Id footer(s):\n'
2299 ' %s\n'
2300 'but issue has Change-Id %s, according to Gerrit.\n'
2301 'Please, check the proposed correction to the description, '
2302 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2303 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2304 change_id))
2305 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2306 if not options.force:
2307 change_desc = ChangeDescription(message)
2308 change_desc.prompt()
2309 message = change_desc.description
2310 if not message:
2311 DieWithError("Description is empty. Aborting...")
2312 # Continue the while loop.
2313 # Sanity check of this code - we should end up with proper message
2314 # footer.
2315 assert [change_id] == git_footers.get_footer_change_id(message)
2316 change_desc = ChangeDescription(message)
2317 else:
2318 change_desc = ChangeDescription(
2319 options.message or CreateDescriptionFromLog(args))
2320 if not options.force:
2321 change_desc.prompt()
2322 if not change_desc.description:
2323 DieWithError("Description is empty. Aborting...")
2324 message = change_desc.description
2325 change_ids = git_footers.get_footer_change_id(message)
2326 if len(change_ids) > 1:
2327 DieWithError('too many Change-Id footers, at most 1 allowed.')
2328 if not change_ids:
2329 # Generate the Change-Id automatically.
2330 message = git_footers.add_footer_change_id(
2331 message, GenerateGerritChangeId(message))
2332 change_desc.set_description(message)
2333 change_ids = git_footers.get_footer_change_id(message)
2334 assert len(change_ids) == 1
2335 change_id = change_ids[0]
2336
2337 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2338 if remote is '.':
2339 # If our upstream branch is local, we base our squashed commit on its
2340 # squashed version.
2341 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2342 # Check the squashed hash of the parent.
2343 parent = RunGit(['config',
2344 'branch.%s.gerritsquashhash' % upstream_branch_name],
2345 error_ok=True).strip()
2346 # Verify that the upstream branch has been uploaded too, otherwise
2347 # Gerrit will create additional CLs when uploading.
2348 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2349 RunGitSilent(['rev-parse', parent + ':'])):
2350 # TODO(tandrii): remove "old depot_tools" part on April 12, 2016.
2351 DieWithError(
2352 'Upload upstream branch %s first.\n'
2353 'Note: maybe you\'ve uploaded it with --no-squash or with an old '
2354 'version of depot_tools. If so, then re-upload it with:\n'
2355 ' git cl upload --squash\n' % upstream_branch_name)
2356 else:
2357 parent = self.GetCommonAncestorWithUpstream()
2358
2359 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2360 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2361 '-m', message]).strip()
2362 else:
2363 change_desc = ChangeDescription(
2364 options.message or CreateDescriptionFromLog(args))
2365 if not change_desc.description:
2366 DieWithError("Description is empty. Aborting...")
2367
2368 if not git_footers.get_footer_change_id(change_desc.description):
2369 DownloadGerritHook(False)
2370 change_desc.set_description(AddChangeIdToCommitMessage(options, args))
2371 ref_to_push = 'HEAD'
2372 parent = '%s/%s' % (gerrit_remote, branch)
2373 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2374
2375 assert change_desc
2376 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2377 ref_to_push)]).splitlines()
2378 if len(commits) > 1:
2379 print('WARNING: This will upload %d commits. Run the following command '
2380 'to see which commits will be uploaded: ' % len(commits))
2381 print('git log %s..%s' % (parent, ref_to_push))
2382 print('You can also use `git squash-branch` to squash these into a '
2383 'single commit.')
2384 ask_for_data('About to upload; enter to confirm.')
2385
2386 if options.reviewers or options.tbr_owners:
2387 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2388 change)
2389
2390 receive_options = []
2391 cc = self.GetCCList().split(',')
2392 if options.cc:
2393 cc.extend(options.cc)
2394 cc = filter(None, cc)
2395 if cc:
2396 receive_options += ['--cc=' + email for email in cc]
2397 if change_desc.get_reviewers():
2398 receive_options.extend(
2399 '--reviewer=' + email for email in change_desc.get_reviewers())
2400
2401 git_command = ['push']
2402 if receive_options:
2403 git_command.append('--receive-pack=git receive-pack %s' %
2404 ' '.join(receive_options))
2405 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
2406 push_stdout = gclient_utils.CheckCallAndFilter(
2407 ['git'] + git_command,
2408 print_stdout=True,
2409 # Flush after every line: useful for seeing progress when running as
2410 # recipe.
2411 filter_fn=lambda _: sys.stdout.flush())
2412
2413 if options.squash:
2414 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2415 change_numbers = [m.group(1)
2416 for m in map(regex.match, push_stdout.splitlines())
2417 if m]
2418 if len(change_numbers) != 1:
2419 DieWithError(
2420 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2421 'Change-Id: %s') % (len(change_numbers), change_id))
2422 self.SetIssue(change_numbers[0])
2423 RunGit(['config', 'branch.%s.gerritsquashhash' % self.GetBranch(),
2424 ref_to_push])
2425 return 0
2426
2427
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002428
2429_CODEREVIEW_IMPLEMENTATIONS = {
2430 'rietveld': _RietveldChangelistImpl,
2431 'gerrit': _GerritChangelistImpl,
2432}
2433
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002434
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002435class ChangeDescription(object):
2436 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002437 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00002438 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002439
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002440 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002441 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002442
agable@chromium.org42c20792013-09-12 17:34:49 +00002443 @property # www.logilab.org/ticket/89786
2444 def description(self): # pylint: disable=E0202
2445 return '\n'.join(self._description_lines)
2446
2447 def set_description(self, desc):
2448 if isinstance(desc, basestring):
2449 lines = desc.splitlines()
2450 else:
2451 lines = [line.rstrip() for line in desc]
2452 while lines and not lines[0]:
2453 lines.pop(0)
2454 while lines and not lines[-1]:
2455 lines.pop(-1)
2456 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002457
piman@chromium.org336f9122014-09-04 02:16:55 +00002458 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002459 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002460 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00002461 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002462 return
agable@chromium.org42c20792013-09-12 17:34:49 +00002463 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002464
agable@chromium.org42c20792013-09-12 17:34:49 +00002465 # Get the set of R= and TBR= lines and remove them from the desciption.
2466 regexp = re.compile(self.R_LINE)
2467 matches = [regexp.match(line) for line in self._description_lines]
2468 new_desc = [l for i, l in enumerate(self._description_lines)
2469 if not matches[i]]
2470 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002471
agable@chromium.org42c20792013-09-12 17:34:49 +00002472 # Construct new unified R= and TBR= lines.
2473 r_names = []
2474 tbr_names = []
2475 for match in matches:
2476 if not match:
2477 continue
2478 people = cleanup_list([match.group(2).strip()])
2479 if match.group(1) == 'TBR':
2480 tbr_names.extend(people)
2481 else:
2482 r_names.extend(people)
2483 for name in r_names:
2484 if name not in reviewers:
2485 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00002486 if add_owners_tbr:
2487 owners_db = owners.Database(change.RepositoryRoot(),
2488 fopen=file, os_path=os.path, glob=glob.glob)
2489 all_reviewers = set(tbr_names + reviewers)
2490 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
2491 all_reviewers)
2492 tbr_names.extend(owners_db.reviewers_for(missing_files,
2493 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00002494 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
2495 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
2496
2497 # Put the new lines in the description where the old first R= line was.
2498 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2499 if 0 <= line_loc < len(self._description_lines):
2500 if new_tbr_line:
2501 self._description_lines.insert(line_loc, new_tbr_line)
2502 if new_r_line:
2503 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002504 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002505 if new_r_line:
2506 self.append_footer(new_r_line)
2507 if new_tbr_line:
2508 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002509
2510 def prompt(self):
2511 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002512 self.set_description([
2513 '# Enter a description of the change.',
2514 '# This will be displayed on the codereview site.',
2515 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002516 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002517 '--------------------',
2518 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002519
agable@chromium.org42c20792013-09-12 17:34:49 +00002520 regexp = re.compile(self.BUG_LINE)
2521 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00002522 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00002523 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002524 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002525 if not content:
2526 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002527 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002528
2529 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00002530 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
2531 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002532 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002533 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002534
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002535 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00002536 if self._description_lines:
2537 # Add an empty line if either the last line or the new line isn't a tag.
2538 last_line = self._description_lines[-1]
2539 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
2540 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2541 self._description_lines.append('')
2542 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002543
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002544 def get_reviewers(self):
2545 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002546 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
2547 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002548 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002549
2550
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002551def get_approving_reviewers(props):
2552 """Retrieves the reviewers that approved a CL from the issue properties with
2553 messages.
2554
2555 Note that the list may contain reviewers that are not committer, thus are not
2556 considered by the CQ.
2557 """
2558 return sorted(
2559 set(
2560 message['sender']
2561 for message in props['messages']
2562 if message['approval'] and message['sender'] in props['reviewers']
2563 )
2564 )
2565
2566
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002567def FindCodereviewSettingsFile(filename='codereview.settings'):
2568 """Finds the given file starting in the cwd and going up.
2569
2570 Only looks up to the top of the repository unless an
2571 'inherit-review-settings-ok' file exists in the root of the repository.
2572 """
2573 inherit_ok_file = 'inherit-review-settings-ok'
2574 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002575 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002576 if os.path.isfile(os.path.join(root, inherit_ok_file)):
2577 root = '/'
2578 while True:
2579 if filename in os.listdir(cwd):
2580 if os.path.isfile(os.path.join(cwd, filename)):
2581 return open(os.path.join(cwd, filename))
2582 if cwd == root:
2583 break
2584 cwd = os.path.dirname(cwd)
2585
2586
2587def LoadCodereviewSettingsFromFile(fileobj):
2588 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00002589 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002590
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002591 def SetProperty(name, setting, unset_error_ok=False):
2592 fullname = 'rietveld.' + name
2593 if setting in keyvals:
2594 RunGit(['config', fullname, keyvals[setting]])
2595 else:
2596 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
2597
2598 SetProperty('server', 'CODE_REVIEW_SERVER')
2599 # Only server setting is required. Other settings can be absent.
2600 # In that case, we ignore errors raised during option deletion attempt.
2601 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002602 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002603 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
2604 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00002605 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002606 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002607 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
2608 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002609 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002610 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002611 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00002612 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
2613 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002614
ukai@chromium.org7044efc2013-11-28 01:51:21 +00002615 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002616 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00002617
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002618 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
2619 RunGit(['config', 'gerrit.squash-uploads',
2620 keyvals['GERRIT_SQUASH_UPLOADS']])
2621
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
2623 #should be of the form
2624 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
2625 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
2626 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
2627 keyvals['ORIGIN_URL_CONFIG']])
2628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002629
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002630def urlretrieve(source, destination):
2631 """urllib is broken for SSL connections via a proxy therefore we
2632 can't use urllib.urlretrieve()."""
2633 with open(destination, 'w') as f:
2634 f.write(urllib2.urlopen(source).read())
2635
2636
ukai@chromium.org712d6102013-11-27 00:52:58 +00002637def hasSheBang(fname):
2638 """Checks fname is a #! script."""
2639 with open(fname) as f:
2640 return f.read(2).startswith('#!')
2641
2642
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00002643# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
2644def DownloadHooks(*args, **kwargs):
2645 pass
2646
2647
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002648def DownloadGerritHook(force):
2649 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002650
2651 Args:
2652 force: True to update hooks. False to install hooks if not present.
2653 """
2654 if not settings.GetIsGerrit():
2655 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00002656 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002657 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2658 if not os.access(dst, os.X_OK):
2659 if os.path.exists(dst):
2660 if not force:
2661 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002662 try:
tandrii@chromium.org18630d62016-03-04 12:06:02 +00002663 print(
2664 'WARNING: installing Gerrit commit-msg hook.\n'
2665 ' This behavior of git cl will soon be disabled.\n'
2666 ' See bug http://crbug.com/579176.')
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00002667 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002668 if not hasSheBang(dst):
2669 DieWithError('Not a script: %s\n'
2670 'You need to download from\n%s\n'
2671 'into .git/hooks/commit-msg and '
2672 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002673 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
2674 except Exception:
2675 if os.path.exists(dst):
2676 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00002677 DieWithError('\nFailed to download hooks.\n'
2678 'You need to download from\n%s\n'
2679 'into .git/hooks/commit-msg and '
2680 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00002681
2682
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002683
2684def GetRietveldCodereviewSettingsInteractively():
2685 """Prompt the user for settings."""
2686 server = settings.GetDefaultServerUrl(error_ok=True)
2687 prompt = 'Rietveld server (host[:port])'
2688 prompt += ' [%s]' % (server or DEFAULT_SERVER)
2689 newserver = ask_for_data(prompt + ':')
2690 if not server and not newserver:
2691 newserver = DEFAULT_SERVER
2692 if newserver:
2693 newserver = gclient_utils.UpgradeToHttps(newserver)
2694 if newserver != server:
2695 RunGit(['config', 'rietveld.server', newserver])
2696
2697 def SetProperty(initial, caption, name, is_url):
2698 prompt = caption
2699 if initial:
2700 prompt += ' ("x" to clear) [%s]' % initial
2701 new_val = ask_for_data(prompt + ':')
2702 if new_val == 'x':
2703 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
2704 elif new_val:
2705 if is_url:
2706 new_val = gclient_utils.UpgradeToHttps(new_val)
2707 if new_val != initial:
2708 RunGit(['config', 'rietveld.' + name, new_val])
2709
2710 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
2711 SetProperty(settings.GetDefaultPrivateFlag(),
2712 'Private flag (rietveld only)', 'private', False)
2713 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
2714 'tree-status-url', False)
2715 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
2716 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
2717 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
2718 'run-post-upload-hook', False)
2719
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002720@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002721def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002722 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002723
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002724 print('WARNING: git cl config works for Rietveld only.\n'
2725 'For Gerrit, see http://crbug.com/579160.')
2726 # TODO(tandrii): add Gerrit support as part of http://crbug.com/579160.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00002727 parser.add_option('--activate-update', action='store_true',
2728 help='activate auto-updating [rietveld] section in '
2729 '.git/config')
2730 parser.add_option('--deactivate-update', action='store_true',
2731 help='deactivate auto-updating [rietveld] section in '
2732 '.git/config')
2733 options, args = parser.parse_args(args)
2734
2735 if options.deactivate_update:
2736 RunGit(['config', 'rietveld.autoupdate', 'false'])
2737 return
2738
2739 if options.activate_update:
2740 RunGit(['config', '--unset', 'rietveld.autoupdate'])
2741 return
2742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002743 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00002744 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002745 return 0
2746
2747 url = args[0]
2748 if not url.endswith('codereview.settings'):
2749 url = os.path.join(url, 'codereview.settings')
2750
2751 # Load code review settings and download hooks (if available).
2752 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
2753 return 0
2754
2755
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002756def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002757 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002758 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
2759 branch = ShortBranchName(branchref)
2760 _, args = parser.parse_args(args)
2761 if not args:
2762 print("Current base-url:")
2763 return RunGit(['config', 'branch.%s.base-url' % branch],
2764 error_ok=False).strip()
2765 else:
2766 print("Setting base-url to %s" % args[0])
2767 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
2768 error_ok=False).strip()
2769
2770
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002771def color_for_status(status):
2772 """Maps a Changelist status to color, for CMDstatus and other tools."""
2773 return {
2774 'unsent': Fore.RED,
2775 'waiting': Fore.BLUE,
2776 'reply': Fore.YELLOW,
2777 'lgtm': Fore.GREEN,
2778 'commit': Fore.MAGENTA,
2779 'closed': Fore.CYAN,
2780 'error': Fore.WHITE,
2781 }.get(status, Fore.WHITE)
2782
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002783def fetch_cl_status(branch, auth_config=None):
2784 """Fetches information for an issue and returns (branch, issue, status)."""
2785 cl = Changelist(branchref=branch, auth_config=auth_config)
2786 url = cl.GetIssueURL()
2787 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002788
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002789 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002790 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002791 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002792
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002793 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002794
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002795def get_cl_statuses(
2796 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002797 """Returns a blocking iterable of (branch, issue, color) for given branches.
2798
2799 If fine_grained is true, this will fetch CL statuses from the server.
2800 Otherwise, simply indicate if there's a matching url for the given branches.
2801
2802 If max_processes is specified, it is used as the maximum number of processes
2803 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
2804 spawned.
2805 """
2806 # Silence upload.py otherwise it becomes unwieldly.
2807 upload.verbosity = 0
2808
2809 if fine_grained:
2810 # Process one branch synchronously to work through authentication, then
2811 # spawn processes to process all the other branches in parallel.
2812 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002813 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
2814 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002815
2816 branches_to_fetch = branches[1:]
2817 pool = ThreadPool(
2818 min(max_processes, len(branches_to_fetch))
2819 if max_processes is not None
2820 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002821 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002822 yield x
2823 else:
2824 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
2825 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002826 cl = Changelist(branchref=b, auth_config=auth_config)
2827 url = cl.GetIssueURL()
2828 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002829
rmistry@google.com2dd99862015-06-22 12:22:18 +00002830
2831def upload_branch_deps(cl, args):
2832 """Uploads CLs of local branches that are dependents of the current branch.
2833
2834 If the local branch dependency tree looks like:
2835 test1 -> test2.1 -> test3.1
2836 -> test3.2
2837 -> test2.2 -> test3.3
2838
2839 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
2840 run on the dependent branches in this order:
2841 test2.1, test3.1, test3.2, test2.2, test3.3
2842
2843 Note: This function does not rebase your local dependent branches. Use it when
2844 you make a change to the parent branch that will not conflict with its
2845 dependent branches, and you would like their dependencies updated in
2846 Rietveld.
2847 """
2848 if git_common.is_dirty_git_tree('upload-branch-deps'):
2849 return 1
2850
2851 root_branch = cl.GetBranch()
2852 if root_branch is None:
2853 DieWithError('Can\'t find dependent branches from detached HEAD state. '
2854 'Get on a branch!')
2855 if not cl.GetIssue() or not cl.GetPatchset():
2856 DieWithError('Current branch does not have an uploaded CL. We cannot set '
2857 'patchset dependencies without an uploaded CL.')
2858
2859 branches = RunGit(['for-each-ref',
2860 '--format=%(refname:short) %(upstream:short)',
2861 'refs/heads'])
2862 if not branches:
2863 print('No local branches found.')
2864 return 0
2865
2866 # Create a dictionary of all local branches to the branches that are dependent
2867 # on it.
2868 tracked_to_dependents = collections.defaultdict(list)
2869 for b in branches.splitlines():
2870 tokens = b.split()
2871 if len(tokens) == 2:
2872 branch_name, tracked = tokens
2873 tracked_to_dependents[tracked].append(branch_name)
2874
2875 print
2876 print 'The dependent local branches of %s are:' % root_branch
2877 dependents = []
2878 def traverse_dependents_preorder(branch, padding=''):
2879 dependents_to_process = tracked_to_dependents.get(branch, [])
2880 padding += ' '
2881 for dependent in dependents_to_process:
2882 print '%s%s' % (padding, dependent)
2883 dependents.append(dependent)
2884 traverse_dependents_preorder(dependent, padding)
2885 traverse_dependents_preorder(root_branch)
2886 print
2887
2888 if not dependents:
2889 print 'There are no dependent local branches for %s' % root_branch
2890 return 0
2891
2892 print ('This command will checkout all dependent branches and run '
2893 '"git cl upload".')
2894 ask_for_data('[Press enter to continue or ctrl-C to quit]')
2895
andybons@chromium.org962f9462016-02-03 20:00:42 +00002896 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00002897 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00002898 args.extend(['-t', 'Updated patchset dependency'])
2899
rmistry@google.com2dd99862015-06-22 12:22:18 +00002900 # Record all dependents that failed to upload.
2901 failures = {}
2902 # Go through all dependents, checkout the branch and upload.
2903 try:
2904 for dependent_branch in dependents:
2905 print
2906 print '--------------------------------------'
2907 print 'Running "git cl upload" from %s:' % dependent_branch
2908 RunGit(['checkout', '-q', dependent_branch])
2909 print
2910 try:
2911 if CMDupload(OptionParser(), args) != 0:
2912 print 'Upload failed for %s!' % dependent_branch
2913 failures[dependent_branch] = 1
2914 except: # pylint: disable=W0702
2915 failures[dependent_branch] = 1
2916 print
2917 finally:
2918 # Swap back to the original root branch.
2919 RunGit(['checkout', '-q', root_branch])
2920
2921 print
2922 print 'Upload complete for dependent branches!'
2923 for dependent_branch in dependents:
2924 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
2925 print ' %s : %s' % (dependent_branch, upload_status)
2926 print
2927
2928 return 0
2929
2930
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002931def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002932 """Show status of changelists.
2933
2934 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00002935 - Red not sent for review or broken
2936 - Blue waiting for review
2937 - Yellow waiting for you to reply to review
2938 - Green LGTM'ed
2939 - Magenta in the commit queue
2940 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002941
2942 Also see 'git cl comments'.
2943 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002944 parser.add_option('--field',
2945 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002946 parser.add_option('-f', '--fast', action='store_true',
2947 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002948 parser.add_option(
2949 '-j', '--maxjobs', action='store', type=int,
2950 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002951
2952 auth.add_auth_options(parser)
2953 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002954 if args:
2955 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002956 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002958 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002959 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002960 if options.field.startswith('desc'):
2961 print cl.GetDescription()
2962 elif options.field == 'id':
2963 issueid = cl.GetIssue()
2964 if issueid:
2965 print issueid
2966 elif options.field == 'patch':
2967 patchset = cl.GetPatchset()
2968 if patchset:
2969 print patchset
2970 elif options.field == 'url':
2971 url = cl.GetIssueURL()
2972 if url:
2973 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002974 return 0
2975
2976 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
2977 if not branches:
2978 print('No local branch found.')
2979 return 0
2980
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002981 changes = (
2982 Changelist(branchref=b, auth_config=auth_config)
2983 for b in branches.splitlines())
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002984 # TODO(tandrii): refactor to use CLs list instead of branches list.
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00002985 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002986 alignment = max(5, max(len(b) for b in branches))
2987 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002988 output = get_cl_statuses(branches,
2989 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002990 max_processes=options.maxjobs,
2991 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002992
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002993 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002994 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00002995 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00002996 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00002997 b, i, status = output.next()
2998 branch_statuses[b] = (i, status)
2999 issue_url, status = branch_statuses.pop(branch)
3000 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003001 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003002 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003003 color = ''
3004 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003005 status_str = '(%s)' % status if status else ''
3006 print ' %*s : %s%s %s%s' % (
3007 alignment, ShortBranchName(branch), color, issue_url, status_str,
3008 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003009
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003010 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003011 print
3012 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003013 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003014 if not cl.GetIssue():
3015 print 'No issue assigned.'
3016 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003017 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00003018 if not options.fast:
3019 print 'Issue description:'
3020 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003021 return 0
3022
3023
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003024def colorize_CMDstatus_doc():
3025 """To be called once in main() to add colors to git cl status help."""
3026 colors = [i for i in dir(Fore) if i[0].isupper()]
3027
3028 def colorize_line(line):
3029 for color in colors:
3030 if color in line.upper():
3031 # Extract whitespaces first and the leading '-'.
3032 indent = len(line) - len(line.lstrip(' ')) + 1
3033 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3034 return line
3035
3036 lines = CMDstatus.__doc__.splitlines()
3037 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3038
3039
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003040@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003041def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003042 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003043
3044 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003045 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003046 parser.add_option('-r', '--reverse', action='store_true',
3047 help='Lookup the branch(es) for the specified issues. If '
3048 'no issues are specified, all branches with mapped '
3049 'issues will be listed.')
3050 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003051
dnj@chromium.org406c4402015-03-03 17:22:28 +00003052 if options.reverse:
3053 branches = RunGit(['for-each-ref', 'refs/heads',
3054 '--format=%(refname:short)']).splitlines()
3055
3056 # Reverse issue lookup.
3057 issue_branch_map = {}
3058 for branch in branches:
3059 cl = Changelist(branchref=branch)
3060 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3061 if not args:
3062 args = sorted(issue_branch_map.iterkeys())
3063 for issue in args:
3064 if not issue:
3065 continue
3066 print 'Branch for issue number %s: %s' % (
3067 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
3068 else:
3069 cl = Changelist()
3070 if len(args) > 0:
3071 try:
3072 issue = int(args[0])
3073 except ValueError:
3074 DieWithError('Pass a number to set the issue or none to list it.\n'
3075 'Maybe you want to run git cl status?')
3076 cl.SetIssue(issue)
3077 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003078 return 0
3079
3080
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003081def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003082 """Shows or posts review comments for any changelist."""
3083 parser.add_option('-a', '--add-comment', dest='comment',
3084 help='comment to add to an issue')
3085 parser.add_option('-i', dest='issue',
3086 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003087 parser.add_option('-j', '--json-file',
3088 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003089 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003090 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003091 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003092
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003093 issue = None
3094 if options.issue:
3095 try:
3096 issue = int(options.issue)
3097 except ValueError:
3098 DieWithError('A review issue id is expected to be a number')
3099
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003100 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003101
3102 if options.comment:
3103 cl.AddComment(options.comment)
3104 return 0
3105
3106 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003107 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003108 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003109 summary.append({
3110 'date': message['date'],
3111 'lgtm': False,
3112 'message': message['text'],
3113 'not_lgtm': False,
3114 'sender': message['sender'],
3115 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003116 if message['disapproval']:
3117 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003118 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003119 elif message['approval']:
3120 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003121 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003122 elif message['sender'] == data['owner_email']:
3123 color = Fore.MAGENTA
3124 else:
3125 color = Fore.BLUE
3126 print '\n%s%s %s%s' % (
3127 color, message['date'].split('.', 1)[0], message['sender'],
3128 Fore.RESET)
3129 if message['text'].strip():
3130 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00003131 if options.json_file:
3132 with open(options.json_file, 'wb') as f:
3133 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003134 return 0
3135
3136
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003137def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003138 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003139 parser.add_option('-d', '--display', action='store_true',
3140 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003141 auth.add_auth_options(parser)
3142 options, _ = parser.parse_args(args)
3143 auth_config = auth.extract_auth_config_from_options(options)
3144 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003145 if not cl.GetIssue():
3146 DieWithError('This branch has no associated changelist.')
3147 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00003148 if options.display:
3149 print description.description
3150 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003151 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003152 if cl.GetDescription() != description.description:
3153 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003154 return 0
3155
3156
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003157def CreateDescriptionFromLog(args):
3158 """Pulls out the commit log to use as a base for the CL description."""
3159 log_args = []
3160 if len(args) == 1 and not args[0].endswith('.'):
3161 log_args = [args[0] + '..']
3162 elif len(args) == 1 and args[0].endswith('...'):
3163 log_args = [args[0][:-1]]
3164 elif len(args) == 2:
3165 log_args = [args[0] + '..' + args[1]]
3166 else:
3167 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003168 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003169
3170
thestig@chromium.org44202a22014-03-11 19:22:18 +00003171def CMDlint(parser, args):
3172 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003173 parser.add_option('--filter', action='append', metavar='-x,+y',
3174 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003175 auth.add_auth_options(parser)
3176 options, args = parser.parse_args(args)
3177 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003178
3179 # Access to a protected member _XX of a client class
3180 # pylint: disable=W0212
3181 try:
3182 import cpplint
3183 import cpplint_chromium
3184 except ImportError:
3185 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
3186 return 1
3187
3188 # Change the current working directory before calling lint so that it
3189 # shows the correct base.
3190 previous_cwd = os.getcwd()
3191 os.chdir(settings.GetRoot())
3192 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003193 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003194 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3195 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003196 if not files:
3197 print "Cannot lint an empty CL"
3198 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003199
3200 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003201 command = args + files
3202 if options.filter:
3203 command = ['--filter=' + ','.join(options.filter)] + command
3204 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003205
3206 white_regex = re.compile(settings.GetLintRegex())
3207 black_regex = re.compile(settings.GetLintIgnoreRegex())
3208 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3209 for filename in filenames:
3210 if white_regex.match(filename):
3211 if black_regex.match(filename):
3212 print "Ignoring file %s" % filename
3213 else:
3214 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3215 extra_check_functions)
3216 else:
3217 print "Skipping file %s" % filename
3218 finally:
3219 os.chdir(previous_cwd)
3220 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
3221 if cpplint._cpplint_state.error_count != 0:
3222 return 1
3223 return 0
3224
3225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003226def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003227 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003228 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003229 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003230 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00003231 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003232 auth.add_auth_options(parser)
3233 options, args = parser.parse_args(args)
3234 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003235
sbc@chromium.org71437c02015-04-09 19:29:40 +00003236 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00003237 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003238 return 1
3239
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003240 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003241 if args:
3242 base_branch = args[0]
3243 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003244 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003245 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00003247 cl.RunHook(
3248 committing=not options.upload,
3249 may_prompt=False,
3250 verbose=options.verbose,
3251 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00003252 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003253
3254
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003255def AddChangeIdToCommitMessage(options, args):
3256 """Re-commits using the current message, assumes the commit hook is in
3257 place.
3258 """
3259 log_desc = options.message or CreateDescriptionFromLog(args)
3260 git_command = ['commit', '--amend', '-m', log_desc]
3261 RunGit(git_command)
3262 new_log_desc = CreateDescriptionFromLog(args)
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003263 if git_footers.get_footer_change_id(new_log_desc):
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003264 print 'git-cl: Added Change-Id to commit message.'
tandrii@chromium.orga342c922016-03-16 07:08:25 +00003265 return new_log_desc
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00003266 else:
3267 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
3268
3269
tandrii@chromium.org65874e12016-03-04 12:03:02 +00003270def GenerateGerritChangeId(message):
3271 """Returns Ixxxxxx...xxx change id.
3272
3273 Works the same way as
3274 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
3275 but can be called on demand on all platforms.
3276
3277 The basic idea is to generate git hash of a state of the tree, original commit
3278 message, author/committer info and timestamps.
3279 """
3280 lines = []
3281 tree_hash = RunGitSilent(['write-tree'])
3282 lines.append('tree %s' % tree_hash.strip())
3283 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
3284 if code == 0:
3285 lines.append('parent %s' % parent.strip())
3286 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
3287 lines.append('author %s' % author.strip())
3288 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
3289 lines.append('committer %s' % committer.strip())
3290 lines.append('')
3291 # Note: Gerrit's commit-hook actually cleans message of some lines and
3292 # whitespace. This code is not doing this, but it clearly won't decrease
3293 # entropy.
3294 lines.append(message)
3295 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
3296 stdin='\n'.join(lines))
3297 return 'I%s' % change_hash.strip()
3298
3299
wittman@chromium.org455dc922015-01-26 20:15:50 +00003300def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
3301 """Computes the remote branch ref to use for the CL.
3302
3303 Args:
3304 remote (str): The git remote for the CL.
3305 remote_branch (str): The git remote branch for the CL.
3306 target_branch (str): The target branch specified by the user.
3307 pending_prefix (str): The pending prefix from the settings.
3308 """
3309 if not (remote and remote_branch):
3310 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003311
wittman@chromium.org455dc922015-01-26 20:15:50 +00003312 if target_branch:
3313 # Cannonicalize branch references to the equivalent local full symbolic
3314 # refs, which are then translated into the remote full symbolic refs
3315 # below.
3316 if '/' not in target_branch:
3317 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
3318 else:
3319 prefix_replacements = (
3320 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
3321 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
3322 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
3323 )
3324 match = None
3325 for regex, replacement in prefix_replacements:
3326 match = re.search(regex, target_branch)
3327 if match:
3328 remote_branch = target_branch.replace(match.group(0), replacement)
3329 break
3330 if not match:
3331 # This is a branch path but not one we recognize; use as-is.
3332 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00003333 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
3334 # Handle the refs that need to land in different refs.
3335 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003336
wittman@chromium.org455dc922015-01-26 20:15:50 +00003337 # Create the true path to the remote branch.
3338 # Does the following translation:
3339 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
3340 # * refs/remotes/origin/master -> refs/heads/master
3341 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
3342 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
3343 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
3344 elif remote_branch.startswith('refs/remotes/%s/' % remote):
3345 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
3346 'refs/heads/')
3347 elif remote_branch.startswith('refs/remotes/branch-heads'):
3348 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
3349 # If a pending prefix exists then replace refs/ with it.
3350 if pending_prefix:
3351 remote_branch = remote_branch.replace('refs/', pending_prefix)
3352 return remote_branch
3353
3354
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003355def cleanup_list(l):
3356 """Fixes a list so that comma separated items are put as individual items.
3357
3358 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
3359 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
3360 """
3361 items = sum((i.split(',') for i in l), [])
3362 stripped_items = (i.strip() for i in items)
3363 return sorted(filter(None, stripped_items))
3364
3365
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003366@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003367def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00003368 """Uploads the current changelist to codereview.
3369
3370 Can skip dependency patchset uploads for a branch by running:
3371 git config branch.branch_name.skip-deps-uploads True
3372 To unset run:
3373 git config --unset branch.branch_name.skip-deps-uploads
3374 Can also set the above globally by using the --global flag.
3375 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00003376 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3377 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00003378 parser.add_option('--bypass-watchlists', action='store_true',
3379 dest='bypass_watchlists',
3380 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003381 parser.add_option('-f', action='store_true', dest='force',
3382 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00003383 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00003384 parser.add_option('-t', dest='title',
3385 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00003386 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003387 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003388 help='reviewer email addresses')
3389 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003390 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00003391 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00003392 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00003393 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003394 parser.add_option('--emulate_svn_auto_props',
3395 '--emulate-svn-auto-props',
3396 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00003397 dest="emulate_svn_auto_props",
3398 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00003399 parser.add_option('-c', '--use-commit-queue', action='store_true',
3400 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003401 parser.add_option('--private', action='store_true',
3402 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00003403 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00003404 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00003405 metavar='TARGET',
3406 help='Apply CL to remote ref TARGET. ' +
3407 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00003408 parser.add_option('--squash', action='store_true',
3409 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003410 parser.add_option('--no-squash', action='store_true',
3411 help='Don\'t squash multiple commits into one ' +
3412 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003413 parser.add_option('--email', default=None,
3414 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00003415 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
3416 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00003417 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
3418 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00003419 help='Send the patchset to do a CQ dry run right after '
3420 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003421 parser.add_option('--dependencies', action='store_true',
3422 help='Uploads CLs of all the local branches that depend on '
3423 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00003424
rmistry@google.com2dd99862015-06-22 12:22:18 +00003425 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003426 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003427 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003428 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003429 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003430
sbc@chromium.org71437c02015-04-09 19:29:40 +00003431 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00003432 return 1
3433
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00003434 options.reviewers = cleanup_list(options.reviewers)
3435 options.cc = cleanup_list(options.cc)
3436
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00003437 # For sanity of test expectations, do this otherwise lazy-loading *now*.
3438 settings.GetIsGerrit()
3439
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003440 cl = Changelist(auth_config=auth_config)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00003441 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00003442
3443
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003444def IsSubmoduleMergeCommit(ref):
3445 # When submodules are added to the repo, we expect there to be a single
3446 # non-git-svn merge commit at remote HEAD with a signature comment.
3447 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00003448 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003449 return RunGit(cmd) != ''
3450
3451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003452def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003453 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003454
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003455 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
3456 upstream and closes the issue automatically and atomically.
3457
3458 Otherwise (in case of Rietveld):
3459 Squashes branch into a single commit.
3460 Updates changelog with metadata (e.g. pointer to review).
3461 Pushes/dcommits the code upstream.
3462 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003463 """
3464 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
3465 help='bypass upload presubmit hook')
3466 parser.add_option('-m', dest='message',
3467 help="override review description")
3468 parser.add_option('-f', action='store_true', dest='force',
3469 help="force yes to questions (don't prompt)")
3470 parser.add_option('-c', dest='contributor',
3471 help="external contributor for patch (appended to " +
3472 "description and used as author for git). Should be " +
3473 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00003474 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003475 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003476 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003477 auth_config = auth.extract_auth_config_from_options(options)
3478
3479 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003480
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003481 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
3482 if cl.IsGerrit():
3483 if options.message:
3484 # This could be implemented, but it requires sending a new patch to
3485 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
3486 # Besides, Gerrit has the ability to change the commit message on submit
3487 # automatically, thus there is no need to support this option (so far?).
3488 parser.error('-m MESSAGE option is not supported for Gerrit.')
3489 if options.contributor:
3490 parser.error(
3491 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
3492 'Before uploading a commit to Gerrit, ensure it\'s author field is '
3493 'the contributor\'s "name <email>". If you can\'t upload such a '
3494 'commit for review, contact your repository admin and request'
3495 '"Forge-Author" permission.')
3496 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
3497 options.verbose)
3498
iannucci@chromium.org5724c962014-04-11 09:32:56 +00003499 current = cl.GetBranch()
3500 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
3501 if not settings.GetIsGitSvn() and remote == '.':
3502 print
3503 print 'Attempting to push branch %r into another local branch!' % current
3504 print
3505 print 'Either reparent this branch on top of origin/master:'
3506 print ' git reparent-branch --root'
3507 print
3508 print 'OR run `git rebase-update` if you think the parent branch is already'
3509 print 'committed.'
3510 print
3511 print ' Current parent: %r' % upstream_branch
3512 return 1
3513
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003514 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003515 # Default to merging against our best guess of the upstream branch.
3516 args = [cl.GetUpstreamBranch()]
3517
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003518 if options.contributor:
3519 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
3520 print "Please provide contibutor as 'First Last <email@example.com>'"
3521 return 1
3522
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003524 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525
sbc@chromium.org71437c02015-04-09 19:29:40 +00003526 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003527 return 1
3528
3529 # This rev-list syntax means "show all commits not in my branch that
3530 # are in base_branch".
3531 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
3532 base_branch]).splitlines()
3533 if upstream_commits:
3534 print ('Base branch "%s" has %d commits '
3535 'not in this branch.' % (base_branch, len(upstream_commits)))
3536 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
3537 return 1
3538
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003539 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003540 svn_head = None
3541 if cmd == 'dcommit' or base_has_submodules:
3542 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
3543 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003544
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003546 # If the base_head is a submodule merge commit, the first parent of the
3547 # base_head should be a git-svn commit, which is what we're interested in.
3548 base_svn_head = base_branch
3549 if base_has_submodules:
3550 base_svn_head += '^1'
3551
3552 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003553 if extra_commits:
3554 print ('This branch has %d additional commits not upstreamed yet.'
3555 % len(extra_commits.splitlines()))
3556 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
3557 'before attempting to %s.' % (base_branch, cmd))
3558 return 1
3559
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003560 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003561 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00003562 author = None
3563 if options.contributor:
3564 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003565 hook_results = cl.RunHook(
3566 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003567 may_prompt=not options.force,
3568 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003569 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00003570 if not hook_results.should_continue():
3571 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003572
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003573 # Check the tree status if the tree status URL is set.
3574 status = GetTreeStatus()
3575 if 'closed' == status:
3576 print('The tree is closed. Please wait for it to reopen. Use '
3577 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3578 return 1
3579 elif 'unknown' == status:
3580 print('Unable to determine tree status. Please verify manually and '
3581 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
3582 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003583
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003584 change_desc = ChangeDescription(options.message)
3585 if not change_desc.description and cl.GetIssue():
3586 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003588 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00003589 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003590 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00003591 else:
3592 print 'No description set.'
3593 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
3594 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003595
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003596 # Keep a separate copy for the commit message, because the commit message
3597 # contains the link to the Rietveld issue, while the Rietveld message contains
3598 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003599 # Keep a separate copy for the commit message.
3600 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00003601 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003602
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003603 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00003604 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00003605 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00003606 # after it. Add a period on a new line to circumvent this. Also add a space
3607 # before the period to make sure that Gitiles continues to correctly resolve
3608 # the URL.
3609 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003610 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003611 commit_desc.append_footer('Patch from %s.' % options.contributor)
3612
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00003613 print('Description:')
3614 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003615
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003616 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003617 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00003618 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003619
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003620 # We want to squash all this branch's commits into one commit with the proper
3621 # description. We do this by doing a "reset --soft" to the base branch (which
3622 # keeps the working copy the same), then dcommitting that. If origin/master
3623 # has a submodule merge commit, we'll also need to cherry-pick the squashed
3624 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003625 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003626 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
3627 # Delete the branches if they exist.
3628 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
3629 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
3630 result = RunGitWithCode(showref_cmd)
3631 if result[0] == 0:
3632 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003633
3634 # We might be in a directory that's present in this branch but not in the
3635 # trunk. Move up to the top of the tree so that git commands that expect a
3636 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003637 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003638 if rel_base_path:
3639 os.chdir(rel_base_path)
3640
3641 # Stuff our change into the merge branch.
3642 # We wrap in a try...finally block so if anything goes wrong,
3643 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003644 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003645 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003646 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003647 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003648 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00003649 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003650 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003651 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003652 RunGit(
3653 [
3654 'commit', '--author', options.contributor,
3655 '-m', commit_desc.description,
3656 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003657 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003658 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003659 if base_has_submodules:
3660 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
3661 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
3662 RunGit(['checkout', CHERRY_PICK_BRANCH])
3663 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003664 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00003665 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003666 mirror = settings.GetGitMirror(remote)
3667 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003668 pending_prefix = settings.GetPendingRefPrefix()
3669 if not pending_prefix or branch.startswith(pending_prefix):
3670 # If not using refs/pending/heads/* at all, or target ref is already set
3671 # to pending, then push to the target ref directly.
3672 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003673 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003674 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003675 else:
3676 # Cherry-pick the change on top of pending ref and then push it.
3677 assert branch.startswith('refs/'), branch
3678 assert pending_prefix[-1] == '/', pending_prefix
3679 pending_ref = pending_prefix + branch[len('refs/'):]
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003680 retcode, output = PushToGitPending(pushurl, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003681 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003682 if retcode == 0:
3683 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003684 else:
3685 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003686 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003687 'svn', 'dcommit',
3688 '-C%s' % options.similarity,
3689 '--no-rebase', '--rmdir',
3690 ]
3691 if settings.GetForceHttpsCommitUrl():
3692 # Allow forcing https commit URLs for some projects that don't allow
3693 # committing to http URLs (like Google Code).
3694 remote_url = cl.GetGitSvnRemoteUrl()
3695 if urlparse.urlparse(remote_url).scheme == 'http':
3696 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00003697 cmd_args.append('--commit-url=%s' % remote_url)
3698 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003699 if 'Committed r' in output:
3700 revision = re.match(
3701 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
3702 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003703 finally:
3704 # And then swap back to the original branch and clean up.
3705 RunGit(['checkout', '-q', cl.GetBranch()])
3706 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00003707 if base_has_submodules:
3708 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003709
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003710 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003711 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00003712 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003713
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003714 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003715 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003716 try:
3717 revision = WaitForRealCommit(remote, revision, base_branch, branch)
3718 # We set pushed_to_pending to False, since it made it all the way to the
3719 # real ref.
3720 pushed_to_pending = False
3721 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003722 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003723
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003724 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003725 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003726 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003727 if not to_pending:
3728 if viewvc_url and revision:
3729 change_desc.append_footer(
3730 'Committed: %s%s' % (viewvc_url, revision))
3731 elif revision:
3732 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003733 print ('Closing issue '
3734 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003735 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003736 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003737 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00003738 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00003739 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00003740 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003741 if options.bypass_hooks:
3742 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
3743 else:
3744 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00003745 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003746 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003747
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003748 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003749 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
3750 print 'The commit is in the pending queue (%s).' % pending_ref
3751 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00003752 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003753 'footer.' % branch)
3754
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00003755 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
3756 if os.path.isfile(hook):
3757 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00003758
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00003759 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003760
3761
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003762def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
3763 print
3764 print 'Waiting for commit to be landed on %s...' % real_ref
3765 print '(If you are impatient, you may Ctrl-C once without harm)'
3766 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
3767 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003768 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003769
3770 loop = 0
3771 while True:
3772 sys.stdout.write('fetching (%d)... \r' % loop)
3773 sys.stdout.flush()
3774 loop += 1
3775
szager@chromium.org151ebcf2016-03-09 01:08:25 +00003776 if mirror:
3777 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003778 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
3779 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
3780 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
3781 for commit in commits.splitlines():
3782 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
3783 print 'Found commit on %s' % real_ref
3784 return commit
3785
3786 current_rev = to_rev
3787
3788
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003789def PushToGitPending(remote, pending_ref, upstream_ref):
3790 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
3791
3792 Returns:
3793 (retcode of last operation, output log of last operation).
3794 """
3795 assert pending_ref.startswith('refs/'), pending_ref
3796 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
3797 cherry = RunGit(['rev-parse', 'HEAD']).strip()
3798 code = 0
3799 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003800 max_attempts = 3
3801 attempts_left = max_attempts
3802 while attempts_left:
3803 if attempts_left != max_attempts:
3804 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
3805 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003806
3807 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003808 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003809 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003810 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003811 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003812 print 'Fetch failed with exit code %d.' % code
3813 if out.strip():
3814 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003815 continue
3816
3817 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003818 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003819 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003820 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003821 if code:
3822 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003823 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3824 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003825 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3826 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003827 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003828 return code, out
3829
3830 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003831 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003832 code, out = RunGitWithCode(
3833 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3834 if code == 0:
3835 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003836 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003837 return code, out
3838
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003839 print 'Push failed with exit code %d.' % code
3840 if out.strip():
3841 print out.strip()
3842 if IsFatalPushFailure(out):
3843 print (
3844 'Fatal push error. Make sure your .netrc credentials and git '
3845 'user.email are correct and you have push access to the repo.')
3846 return code, out
3847
3848 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003849 return code, out
3850
3851
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003852def IsFatalPushFailure(push_stdout):
3853 """True if retrying push won't help."""
3854 return '(prohibited by Gerrit)' in push_stdout
3855
3856
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003857@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003858def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003859 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003860 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003861 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003862 # If it looks like previous commits were mirrored with git-svn.
3863 message = """This repository appears to be a git-svn mirror, but no
3864upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3865 else:
3866 message = """This doesn't appear to be an SVN repository.
3867If your project has a true, writeable git repository, you probably want to run
3868'git cl land' instead.
3869If your project has a git mirror of an upstream SVN master, you probably need
3870to run 'git svn init'.
3871
3872Using the wrong command might cause your commit to appear to succeed, and the
3873review to be closed, without actually landing upstream. If you choose to
3874proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003875 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003876 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003877 return SendUpstream(parser, args, 'dcommit')
3878
3879
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003880@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003881def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003882 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00003883 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003884 print('This appears to be an SVN repository.')
3885 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003886 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003887 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003888 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003889
3890
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003891@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003892def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003893 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003894 parser.add_option('-b', dest='newbranch',
3895 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003896 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003897 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003898 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3899 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003900 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003901 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003902 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003903 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003904 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003905 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003906
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003907
3908 group = optparse.OptionGroup(
3909 parser,
3910 'Options for continuing work on the current issue uploaded from a '
3911 'different clone (e.g. different machine). Must be used independently '
3912 'from the other options. No issue number should be specified, and the '
3913 'branch must have an issue number associated with it')
3914 group.add_option('--reapply', action='store_true', dest='reapply',
3915 help='Reset the branch and reapply the issue.\n'
3916 'CAUTION: This will undo any local changes in this '
3917 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003918
3919 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003920 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003921 parser.add_option_group(group)
3922
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003923 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003924 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003925 auth_config = auth.extract_auth_config_from_options(options)
3926
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003927 cl = Changelist(auth_config=auth_config)
3928
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003929 issue_arg = None
3930 if options.reapply :
3931 if len(args) > 0:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003932 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003933
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003934 issue_arg = cl.GetIssue()
3935 upstream = cl.GetUpstreamBranch()
3936 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003937 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003938
3939 RunGit(['reset', '--hard', upstream])
3940 if options.pull:
3941 RunGit(['pull'])
3942 else:
3943 if len(args) != 1:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003944 parser.error('Must specify issue number or url')
3945 issue_arg = args[0]
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003946
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003947 if not issue_arg:
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003948 parser.print_help()
3949 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003950
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003951 if cl.IsGerrit():
3952 if options.reject:
3953 parser.error('--reject is not supported with Gerrit codereview.')
3954 if options.nocommit:
3955 parser.error('--nocommit is not supported with Gerrit codereview.')
3956 if options.directory:
3957 parser.error('--directory is not supported with Gerrit codereview.')
3958
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003959 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003960 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003961 return 1
3962
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003963 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003964 if options.reapply:
3965 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003966 if options.force:
3967 RunGit(['branch', '-D', options.newbranch],
3968 stderr=subprocess2.PIPE, error_ok=True)
3969 RunGit(['checkout', '-b', options.newbranch,
3970 Changelist().GetUpstreamBranch()])
3971
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00003972 return cl.CMDPatchIssue(issue_arg, options.reject, options.nocommit,
3973 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003974
3975
3976def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003977 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003978 # Provide a wrapper for git svn rebase to help avoid accidental
3979 # git svn dcommit.
3980 # It's the only command that doesn't use parser at all since we just defer
3981 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003982
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003983 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984
3985
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003986def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003987 """Fetches the tree status and returns either 'open', 'closed',
3988 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003989 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003990 if url:
3991 status = urllib2.urlopen(url).read().lower()
3992 if status.find('closed') != -1 or status == '0':
3993 return 'closed'
3994 elif status.find('open') != -1 or status == '1':
3995 return 'open'
3996 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003997 return 'unset'
3998
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003999
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004000def GetTreeStatusReason():
4001 """Fetches the tree status from a json url and returns the message
4002 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004003 url = settings.GetTreeStatusUrl()
4004 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004005 connection = urllib2.urlopen(json_url)
4006 status = json.loads(connection.read())
4007 connection.close()
4008 return status['message']
4009
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004010
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004011def GetBuilderMaster(bot_list):
4012 """For a given builder, fetch the master from AE if available."""
4013 map_url = 'https://builders-map.appspot.com/'
4014 try:
4015 master_map = json.load(urllib2.urlopen(map_url))
4016 except urllib2.URLError as e:
4017 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
4018 (map_url, e))
4019 except ValueError as e:
4020 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
4021 if not master_map:
4022 return None, 'Failed to build master map.'
4023
4024 result_master = ''
4025 for bot in bot_list:
4026 builder = bot.split(':', 1)[0]
4027 master_list = master_map.get(builder, [])
4028 if not master_list:
4029 return None, ('No matching master for builder %s.' % builder)
4030 elif len(master_list) > 1:
4031 return None, ('The builder name %s exists in multiple masters %s.' %
4032 (builder, master_list))
4033 else:
4034 cur_master = master_list[0]
4035 if not result_master:
4036 result_master = cur_master
4037 elif result_master != cur_master:
4038 return None, 'The builders do not belong to the same master.'
4039 return result_master, None
4040
4041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004042def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004043 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004044 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004045 status = GetTreeStatus()
4046 if 'unset' == status:
4047 print 'You must configure your tree status URL by running "git cl config".'
4048 return 2
4049
4050 print "The tree is %s" % status
4051 print
4052 print GetTreeStatusReason()
4053 if status != 'open':
4054 return 1
4055 return 0
4056
4057
maruel@chromium.org15192402012-09-06 12:38:29 +00004058def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004059 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00004060 group = optparse.OptionGroup(parser, "Try job options")
4061 group.add_option(
4062 "-b", "--bot", action="append",
4063 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
4064 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004065 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00004066 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004067 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00004068 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004069 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00004070 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004071 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004072 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004073 "-r", "--revision",
4074 help="Revision to use for the try job; default: the "
4075 "revision will be determined by the try server; see "
4076 "its waterfall for more info")
4077 group.add_option(
4078 "-c", "--clobber", action="store_true", default=False,
4079 help="Force a clobber before building; e.g. don't do an "
4080 "incremental build")
4081 group.add_option(
4082 "--project",
4083 help="Override which project to use. Projects are defined "
4084 "server-side to define what default bot set to use")
4085 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00004086 "-p", "--property", dest="properties", action="append", default=[],
4087 help="Specify generic properties in the form -p key1=value1 -p "
4088 "key2=value2 etc (buildbucket only). The value will be treated as "
4089 "json if decodable, or as string otherwise.")
4090 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00004091 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004092 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004093 "--use-rietveld", action="store_true", default=False,
4094 help="Use Rietveld to trigger try jobs.")
4095 group.add_option(
4096 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4097 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00004098 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004099 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004100 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004101 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004102
machenbach@chromium.org45453142015-09-15 08:45:22 +00004103 if options.use_rietveld and options.properties:
4104 parser.error('Properties can only be specified with buildbucket')
4105
4106 # Make sure that all properties are prop=value pairs.
4107 bad_params = [x for x in options.properties if '=' not in x]
4108 if bad_params:
4109 parser.error('Got properties with missing "=": %s' % bad_params)
4110
maruel@chromium.org15192402012-09-06 12:38:29 +00004111 if args:
4112 parser.error('Unknown arguments: %s' % args)
4113
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004114 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004115 if not cl.GetIssue():
4116 parser.error('Need to upload first')
4117
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004118 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00004119 if props.get('closed'):
4120 parser.error('Cannot send tryjobs for a closed CL')
4121
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004122 if props.get('private'):
4123 parser.error('Cannot use trybots with private issue')
4124
maruel@chromium.org15192402012-09-06 12:38:29 +00004125 if not options.name:
4126 options.name = cl.GetBranch()
4127
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004128 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00004129 options.master, err_msg = GetBuilderMaster(options.bot)
4130 if err_msg:
4131 parser.error('Tryserver master cannot be found because: %s\n'
4132 'Please manually specify the tryserver master'
4133 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004134
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004135 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004136 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004137 if not options.bot:
4138 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00004139
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004140 # Get try masters from PRESUBMIT.py files.
4141 masters = presubmit_support.DoGetTryMasters(
4142 change,
4143 change.LocalPaths(),
4144 settings.GetRoot(),
4145 None,
4146 None,
4147 options.verbose,
4148 sys.stdout)
4149 if masters:
4150 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00004151
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004152 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
4153 options.bot = presubmit_support.DoGetTrySlaves(
4154 change,
4155 change.LocalPaths(),
4156 settings.GetRoot(),
4157 None,
4158 None,
4159 options.verbose,
4160 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004161
4162 if not options.bot:
4163 # Get try masters from cq.cfg if any.
4164 # TODO(tandrii): some (but very few) projects store cq.cfg in different
4165 # location.
4166 cq_cfg = os.path.join(change.RepositoryRoot(),
4167 'infra', 'config', 'cq.cfg')
4168 if os.path.exists(cq_cfg):
4169 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00004170 cq_masters = commit_queue.get_master_builder_map(
4171 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00004172 for master, builders in cq_masters.iteritems():
4173 for builder in builders:
4174 # Skip presubmit builders, because these will fail without LGTM.
4175 if 'presubmit' not in builder.lower():
4176 masters.setdefault(master, {})[builder] = ['defaulttests']
4177 if masters:
4178 return masters
4179
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004180 if not options.bot:
4181 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00004182
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004183 builders_and_tests = {}
4184 # TODO(machenbach): The old style command-line options don't support
4185 # multiple try masters yet.
4186 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
4187 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
4188
4189 for bot in old_style:
4190 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00004191 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004192 elif ',' in bot:
4193 parser.error('Specify one bot per --bot flag')
4194 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00004195 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004196
4197 for bot, tests in new_style:
4198 builders_and_tests.setdefault(bot, []).extend(tests)
4199
4200 # Return a master map with one master to be backwards compatible. The
4201 # master name defaults to an empty string, which will cause the master
4202 # not to be set on rietveld (deprecated).
4203 return {options.master: builders_and_tests}
4204
4205 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004206
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004207 for builders in masters.itervalues():
4208 if any('triggered' in b for b in builders):
4209 print >> sys.stderr, (
4210 'ERROR You are trying to send a job to a triggered bot. This type of'
4211 ' bot requires an\ninitial job from a parent (usually a builder). '
4212 'Instead send your job to the parent.\n'
4213 'Bot list: %s' % builders)
4214 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004215
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004216 patchset = cl.GetMostRecentPatchset()
4217 if patchset and patchset != cl.GetPatchset():
4218 print(
4219 '\nWARNING Mismatch between local config and server. Did a previous '
4220 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4221 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00004222 if options.luci:
4223 trigger_luci_job(cl, masters, options)
4224 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004225 try:
4226 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
4227 except BuildbucketResponseException as ex:
4228 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00004229 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004230 except Exception as e:
4231 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4232 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
4233 e, stacktrace)
4234 return 1
4235 else:
4236 try:
4237 cl.RpcServer().trigger_distributed_try_jobs(
4238 cl.GetIssue(), patchset, options.name, options.clobber,
4239 options.revision, masters)
4240 except urllib2.HTTPError as e:
4241 if e.code == 404:
4242 print('404 from rietveld; '
4243 'did you mean to use "git try" instead of "git cl try"?')
4244 return 1
4245 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004246
sheyang@google.com6ebaf782015-05-12 19:17:54 +00004247 for (master, builders) in sorted(masters.iteritems()):
4248 if master:
4249 print 'Master: %s' % master
4250 length = max(len(builder) for builder in builders)
4251 for builder in sorted(builders):
4252 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00004253 return 0
4254
4255
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004256def CMDtry_results(parser, args):
4257 group = optparse.OptionGroup(parser, "Try job results options")
4258 group.add_option(
4259 "-p", "--patchset", type=int, help="patchset number if not current.")
4260 group.add_option(
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004261 "--print-master", action='store_true', help="print master name as well.")
4262 group.add_option(
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004263 "--color", action='store_true', default=setup_color.IS_TTY,
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004264 help="force color output, useful when piping output.")
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004265 group.add_option(
4266 "--buildbucket-host", default='cr-buildbucket.appspot.com',
4267 help="Host of buildbucket. The default host is %default.")
4268 parser.add_option_group(group)
4269 auth.add_auth_options(parser)
4270 options, args = parser.parse_args(args)
4271 if args:
4272 parser.error('Unrecognized args: %s' % ' '.join(args))
4273
4274 auth_config = auth.extract_auth_config_from_options(options)
4275 cl = Changelist(auth_config=auth_config)
4276 if not cl.GetIssue():
4277 parser.error('Need to upload first')
4278
4279 if not options.patchset:
4280 options.patchset = cl.GetMostRecentPatchset()
4281 if options.patchset and options.patchset != cl.GetPatchset():
4282 print(
4283 '\nWARNING Mismatch between local config and server. Did a previous '
4284 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
4285 'Continuing using\npatchset %s.\n' % options.patchset)
4286 try:
4287 jobs = fetch_try_jobs(auth_config, cl, options)
4288 except BuildbucketResponseException as ex:
4289 print 'Buildbucket error: %s' % ex
4290 return 1
4291 except Exception as e:
4292 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
4293 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
4294 e, stacktrace)
4295 return 1
4296 print_tryjobs(options, jobs)
4297 return 0
4298
4299
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004300@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004302 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004303 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004304 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004305 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004306
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004307 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004308 if args:
4309 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004310 branch = cl.GetBranch()
4311 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004312 cl = Changelist()
4313 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004314
4315 # Clear configured merge-base, if there is one.
4316 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004317 else:
4318 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004319 return 0
4320
4321
thestig@chromium.org00858c82013-12-02 23:08:03 +00004322def CMDweb(parser, args):
4323 """Opens the current CL in the web browser."""
4324 _, args = parser.parse_args(args)
4325 if args:
4326 parser.error('Unrecognized args: %s' % ' '.join(args))
4327
4328 issue_url = Changelist().GetIssueURL()
4329 if not issue_url:
4330 print >> sys.stderr, 'ERROR No issue to open'
4331 return 1
4332
4333 webbrowser.open(issue_url)
4334 return 0
4335
4336
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004337def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004338 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004339 auth.add_auth_options(parser)
4340 options, args = parser.parse_args(args)
4341 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004342 if args:
4343 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004344 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004345 props = cl.GetIssueProperties()
4346 if props.get('private'):
4347 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004348 cl.SetFlag('commit', '1')
4349 return 0
4350
4351
groby@chromium.org411034a2013-02-26 15:12:01 +00004352def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004353 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004354 auth.add_auth_options(parser)
4355 options, args = parser.parse_args(args)
4356 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004357 if args:
4358 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004359 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00004360 # Ensure there actually is an issue to close.
4361 cl.GetDescription()
4362 cl.CloseIssue()
4363 return 0
4364
4365
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004366def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004367 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004368 auth.add_auth_options(parser)
4369 options, args = parser.parse_args(args)
4370 auth_config = auth.extract_auth_config_from_options(options)
4371 if args:
4372 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004373
4374 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004375 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004376 # Staged changes would be committed along with the patch from last
4377 # upload, hence counted toward the "last upload" side in the final
4378 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00004379 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004380 return 1
4381
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004382 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004383 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004384 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004385 if not issue:
4386 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004387 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004388 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004389
4390 # Create a new branch based on the merge-base
4391 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00004392 # Clear cached branch in cl object, to avoid overwriting original CL branch
4393 # properties.
4394 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004395 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004396 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004397 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00004398 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004399 return rtn
4400
wychen@chromium.org06928532015-02-03 02:11:29 +00004401 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004402 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00004403 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004404 finally:
4405 RunGit(['checkout', '-q', branch])
4406 RunGit(['branch', '-D', TMP_BRANCH])
4407
4408 return 0
4409
4410
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004411def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004412 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004413 parser.add_option(
4414 '--no-color',
4415 action='store_true',
4416 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004417 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004418 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004419 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004420
4421 author = RunGit(['config', 'user.email']).strip() or None
4422
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004423 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004424
4425 if args:
4426 if len(args) > 1:
4427 parser.error('Unknown args')
4428 base_branch = args[0]
4429 else:
4430 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004431 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004432
4433 change = cl.GetChange(base_branch, None)
4434 return owners_finder.OwnersFinder(
4435 [f.LocalPath() for f in
4436 cl.GetChange(base_branch, None).AffectedFiles()],
4437 change.RepositoryRoot(), author,
4438 fopen=file, os_path=os.path, glob=glob.glob,
4439 disable_color=options.no_color).run()
4440
4441
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004442def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004443 """Generates a diff command."""
4444 # Generate diff for the current branch's changes.
4445 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
4446 upstream_commit, '--' ]
4447
4448 if args:
4449 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004450 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004451 diff_cmd.append(arg)
4452 else:
4453 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004454
4455 return diff_cmd
4456
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004457def MatchingFileType(file_name, extensions):
4458 """Returns true if the file name ends with one of the given extensions."""
4459 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004460
enne@chromium.org555cfe42014-01-29 18:21:39 +00004461@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004462def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004463 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00004464 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004465 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00004466 parser.add_option('--full', action='store_true',
4467 help='Reformat the full content of all touched files')
4468 parser.add_option('--dry-run', action='store_true',
4469 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004470 parser.add_option('--python', action='store_true',
4471 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00004472 parser.add_option('--diff', action='store_true',
4473 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004474 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004475
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004476 # git diff generates paths against the root of the repository. Change
4477 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004478 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00004479 if rel_base_path:
4480 os.chdir(rel_base_path)
4481
digit@chromium.org29e47272013-05-17 17:01:46 +00004482 # Grab the merge-base commit, i.e. the upstream commit of the current
4483 # branch when it was created or the last time it was rebased. This is
4484 # to cover the case where the user may have called "git fetch origin",
4485 # moving the origin branch to a newer commit, but hasn't rebased yet.
4486 upstream_commit = None
4487 cl = Changelist()
4488 upstream_branch = cl.GetUpstreamBranch()
4489 if upstream_branch:
4490 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
4491 upstream_commit = upstream_commit.strip()
4492
4493 if not upstream_commit:
4494 DieWithError('Could not find base commit for this branch. '
4495 'Are you in detached state?')
4496
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004497 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
4498 diff_output = RunGit(changed_files_cmd)
4499 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00004500 # Filter out files deleted by this CL
4501 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004502
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004503 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
4504 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
4505 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004506 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00004507
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00004508 top_dir = os.path.normpath(
4509 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
4510
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004511 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
4512 # formatted. This is used to block during the presubmit.
4513 return_value = 0
4514
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004515 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00004516 # Locate the clang-format binary in the checkout
4517 try:
4518 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
4519 except clang_format.NotFoundError, e:
4520 DieWithError(e)
4521
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004522 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004523 cmd = [clang_format_tool]
4524 if not opts.dry_run and not opts.diff:
4525 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004526 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004527 if opts.diff:
4528 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004529 else:
4530 env = os.environ.copy()
4531 env['PATH'] = str(os.path.dirname(clang_format_tool))
4532 try:
4533 script = clang_format.FindClangFormatScriptInChromiumTree(
4534 'clang-format-diff.py')
4535 except clang_format.NotFoundError, e:
4536 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004537
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004538 cmd = [sys.executable, script, '-p0']
4539 if not opts.dry_run and not opts.diff:
4540 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00004541
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004542 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
4543 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004544
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00004545 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
4546 if opts.diff:
4547 sys.stdout.write(stdout)
4548 if opts.dry_run and len(stdout) > 0:
4549 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004550
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004551 # Similar code to above, but using yapf on .py files rather than clang-format
4552 # on C/C++ files
4553 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004554 yapf_tool = gclient_utils.FindExecutable('yapf')
4555 if yapf_tool is None:
4556 DieWithError('yapf not found in PATH')
4557
4558 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004559 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004560 cmd = [yapf_tool]
4561 if not opts.dry_run and not opts.diff:
4562 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004563 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00004564 if opts.diff:
4565 sys.stdout.write(stdout)
4566 else:
4567 # TODO(sbc): yapf --lines mode still has some issues.
4568 # https://github.com/google/yapf/issues/154
4569 DieWithError('--python currently only works with --full')
4570
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004571 # Dart's formatter does not have the nice property of only operating on
4572 # modified chunks, so hard code full.
4573 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004574 try:
4575 command = [dart_format.FindDartFmtToolInChromiumTree()]
4576 if not opts.dry_run and not opts.diff:
4577 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00004578 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004579
ppi@chromium.org6593d932016-03-03 15:41:15 +00004580 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004581 if opts.dry_run and stdout:
4582 return_value = 2
4583 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00004584 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
4585 'found in this checkout. Files in other languages are still ' +
4586 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004587
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00004588 # Format GN build files. Always run on full build files for canonical form.
4589 if gn_diff_files:
4590 cmd = ['gn', 'format']
4591 if not opts.dry_run and not opts.diff:
4592 cmd.append('--in-place')
4593 for gn_diff_file in gn_diff_files:
4594 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
4595 if opts.diff:
4596 sys.stdout.write(stdout)
4597
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00004598 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00004599
4600
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004601@subcommand.usage('<codereview url or issue id>')
4602def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004603 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004604 _, args = parser.parse_args(args)
4605
4606 if len(args) != 1:
4607 parser.print_help()
4608 return 1
4609
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004610 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00004611 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004612 parser.print_help()
4613 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00004614 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004615
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004616 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00004617 output = RunGit(['config', '--local', '--get-regexp',
4618 r'branch\..*\.%s' % issueprefix],
4619 error_ok=True)
4620 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004621 if issue == target_issue:
4622 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004623
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00004624 branches = []
4625 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii@chromium.orgd03bc632016-04-12 14:17:26 +00004626 branches.extend(find_issues(cls.IssueSettingSuffix()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00004627 if len(branches) == 0:
4628 print 'No branch found for issue %s.' % target_issue
4629 return 1
4630 if len(branches) == 1:
4631 RunGit(['checkout', branches[0]])
4632 else:
4633 print 'Multiple branches match issue %s:' % target_issue
4634 for i in range(len(branches)):
4635 print '%d: %s' % (i, branches[i])
4636 which = raw_input('Choose by index: ')
4637 try:
4638 RunGit(['checkout', branches[int(which)]])
4639 except (IndexError, ValueError):
4640 print 'Invalid selection, not checking out any branch.'
4641 return 1
4642
4643 return 0
4644
4645
maruel@chromium.org29404b52014-09-08 22:58:00 +00004646def CMDlol(parser, args):
4647 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00004648 print zlib.decompress(base64.b64decode(
4649 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
4650 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
4651 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
4652 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00004653 return 0
4654
4655
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004656class OptionParser(optparse.OptionParser):
4657 """Creates the option parse and add --verbose support."""
4658 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004659 optparse.OptionParser.__init__(
4660 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004661 self.add_option(
4662 '-v', '--verbose', action='count', default=0,
4663 help='Use 2 times for more debugging info')
4664
4665 def parse_args(self, args=None, values=None):
4666 options, args = optparse.OptionParser.parse_args(self, args, values)
4667 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
4668 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
4669 return options, args
4670
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004671
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004672def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00004673 if sys.hexversion < 0x02060000:
4674 print >> sys.stderr, (
4675 '\nYour python version %s is unsupported, please upgrade.\n' %
4676 sys.version.split(' ', 1)[0])
4677 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004678
maruel@chromium.orgddd59412011-11-30 14:20:38 +00004679 # Reload settings.
4680 global settings
4681 settings = Settings()
4682
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004683 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004684 dispatcher = subcommand.CommandDispatcher(__name__)
4685 try:
4686 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00004687 except auth.AuthenticationError as e:
4688 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004689 except urllib2.HTTPError, e:
4690 if e.code != 500:
4691 raise
4692 DieWithError(
4693 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
4694 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00004695 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004696
4697
4698if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00004699 # These affect sys.stdout so do it outside of main() to simplify mocks in
4700 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00004701 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004702 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00004703 try:
4704 sys.exit(main(sys.argv[1:]))
4705 except KeyboardInterrupt:
4706 sys.stderr.write('interrupted\n')
4707 sys.exit(1)